maidr (development version)
New Features
Chart size
- A chart’s size can be set, in inches as
ggplot2::ggsave()and knitr’sfig.widthtake one:show()andsave_html()takewidthandheight, andrender_maidr()takesfig_widthandfig_height, whilemaidr_output()’swidthandheightstill size the output on the page. The SVG is 72 pixels to the inch. Every chart used to be drawn at 7 x 5 in whatever was asked, and awidthgiven toshow()orsave_html()was silently ignored, except by the static picture of a chart maidr could not read. Unset, the size is still 7 x 5 in (#355). - In R Markdown and Quarto a chart is drawn at its chunk’s
fig.widthandfig.height, sofig.asp,fig.dim, YAMLfig_widthandfig_heightand Quarto’sfig-widthandfig-heightset it too.html_documentand Quarto’s HTML draw figures at 7 x 5 in, so their charts keep the size they had. A format with a figure size of its own now draws its charts at it:html_vignetteat 3 x 3 in, where a Base R chart shows fewer tick labels, ioslides at 7.5 x 4.5 in, slidy at 8 x 6 in (#355). - A candlestick chart is still never drawn smaller than 12 x 6 in, and a smaller size asked for is enlarged with a message naming the size used. In a document that is a size the chunk sets itself, and the message is among the chunk’s own for a ggplot2 chart and on the console for Base R’s
chartSeries(); drawn larger than the document’s figure size, a candlestick chart says nothing, as before (#355). - A size that is not one positive number is an error naming the argument, and so is one over 50 in, which
ggplot2::ggsave()refuses too: the error says the size is in inches, not the pixelsmaidr_output()takes. A Base R chart too small for its margins and text at a size asked for, which R cannot draw at that size either (“figure margins too large”), is an error naming the size and R’s reason rather than an empty chart, or a blank picture of a chart maidr cannot read (#355). - A Base R chart too small for a size no one asked for – maidr’s own 7 x 5 in, or in a document the figure size every chunk that sets none is drawn at – is drawn larger, with a message naming the size. It is drawn on the 7 x 7 in page maidr laid every Base R chart out on before it drew one at its size, as a
par(mfrow)grid of five rows was. Where that is too small too, it is drawn on the smallest larger page that leaves each of its plots a sixth of an inch, 12 px, each way, each side grown in whole inches only as far as it needs: 7 x 9 in for six rows, 12 x 5 in for twelve columns. The picture of a chart maidr cannot read, such as a grid ofpersp()plots, is drawn larger in the same way (#355). - The size changes how a chart is laid out and nothing a reader hears: the data, titles and axis labels are the same at every size, apart from the page coordinates a violin’s density curve carries for its highlight. A lattice chart conditioned on one variable with no
layout =gets the columns lattice gives a page of its shape, and its subplot grid, the order a reader moves through its panels in, with them: two panels are 1 x 2 at 7 x 5 in and 2 x 1 at 5 x 8 in (#355). -
show(as_widget = TRUE)no longer passeswidthandheighton to the widget as its CSS size, which it did through...without saying so: they are the size the chart is drawn at, in inches, as everywhere inshow(). A CSS size such as"300px"is now an error that says so. Set the widget’s own size on the widgetshow()returns,widget$width <- "300px"(#355).
R Markdown and Quarto
-
library(maidr)is all an R Markdown or Quarto document needs: every plot it draws, with ‘ggplot2’, ‘lattice’ or Base R, becomes an accessible chart, withoutmaidr_on()in a setup chunk (#352). maidr installs its ‘knitr’ hooks into the knit when the document loads it, or at the first chart the document draws, and does so again in every render of a session, so a secondrmarkdown::render(), andR CMD build, which renders every vignette in one process, get charts too.maidr_on()still works, andmaidr_off()in a chunk knits the chunks after it as they would be without maidr; it lasts for the R session, so a document that turns maidr off, and every document rendered after it in the session, stays off untilmaidr_on(). A document rendered in a session where maidr is loaded, even only its namespace, as by a package that imports it, gets charts whether or not it loads maidr itself;options(maidr.auto_show = FALSE)prevents that. maidr’s startup message is no longer written into a document. - In HTML output a chart is now part of the page, an
<svg>in a raw HTML block, where it used to be an iframe that loaded maidr.js for itself. The page loads maidr.js once however many charts it has, from its_filesfolder or embedded in aself_contained/embed-resourcespage, where a document rendered offline used to carry a copy of the bundle in every chart’s frame. Every id of a chart is given a prefix of its own, so charts sharing a page never reach into one another. This covershtml_documentand the formats built on it, ‘bookdown’, Quarto (HTML, reveal.js, dashboards and websites), ‘revealjs’, ioslides, slidy and ‘flexdashboard’. Before maidr.js has loaded, and if it never does, a chart is an image named byfig.alt,fig.cap, its title or its kind; once it has,fig.altand the caption describe it. maidr’s help, settings and chat open in the page at their own size, on a Bootstrap 3 page (html_document‘s default theme) as on any other, and within the window in a reveal.js deck. Once a page shows a maidr chart, maidr.js also reads the plain ’plotly’ widgets on it, which become maidr charts with a tab stop of their own, as they did on a page holding amaidr_htmlwidget(). An HTML fragment, ‘pagedown’ and.Rhtmlkeep each chart in an iframe of its own, as does a chart that cannot be shown inline, with a warning. In EPUB and ‘xaringan’ output, as in PDF, Word and Markdown, the plots are knitr’s figures, where a document that calledmaidr_on()used to stop with an error (EPUB) or show each chart’s iframe as text on its slide (‘xaringan’). - In HTML output ‘knitr’ records a chunk’s figures with ‘svglite’ instead of its default png, so the figures that stay images are vector images too. Only the default is replaced: a device a chunk names, a document device other than png, and the device of a cached chunk, of an animation and of a chunk that crops its figures or hands them to
fig.processare kept.options(maidr.knitr_dev = FALSE)keeps png for every chunk, for a document whosedev: pngis a choice, which cannot be told from the default; see?"maidr-options". A ‘flexdashboard’ gets SVG figures too, with no separate copy for phones. - A chunk can draw several charts. A ggplot2 or lattice chart printed with
print(), in a loop or a function, used to stay a static image, and the Base R charts of a chunk all became its first figure. Now each chart takes the place of its own figure, sofig.cap,fig.alt,fig.keep,fig.show, thefig:label-1,fig:label-2labels of ‘bookdown’ and Quarto’s@fig-references apply to it as they apply to the figure, and charts held withfig.show = "hold"at anout.widthsit side by side, as knitr’s images do. A chart a chunk returns is one of its figures too, numbered and captioned with the others, andresults = "hide"no longer drops it. A figure that is not one chart maidr can read – two charts on one page, a chart something was drawn over, a grid drawing, a chart type maidr does not read – stays knitr’s image rather than risk showing the wrong chart. A chunk cached withcache = TRUEbrings its charts back; one cached withcache = 1orcache = 2brings back static figures, andfig.show = "animate"stays knitr’s animation. In R Markdown and bookdown a chart’s caption is plain text, Markdown, maths and\@ref()in it shown as written; a bookdown text reference,fig.cap = "(ref:label)", brings them in. - The keys of the page around a chart no longer act while the chart has the focus. A chart in a frame had its keys to itself; inline, the arrow keys, Space and the other shortcuts of a reveal.js deck, the gitbook format of ‘bookdown’, a Quarto website’s search, an ioslides or slidy deck, a ‘flexdashboard’ storyboard and the
/search key of a ‘pkgdown’ site would have acted on the keys maidr reads. Tab moves into a chart and Shift+Tab out of it, after which the page’s keys work again. The focus ring stays visible on a page whose CSS removes outlines, and in forced colours.
lattice
Charts drawn by ‘lattice’ are now read (#333).
show(),save_html(),show(as_widget = TRUE),render_maidr()in Shiny, and R Markdown and Quarto take a trellis object, and printing one at the console opens it in the maidr viewer, as printing a ggplot2 object does. Every lattice reading is experimental: none has been through a user study, and each may change without a deprecation period. ‘lattice’ is now in Suggests, as is ‘latticeExtra’, which only the tests use.barchart()is emitted as abarlayer, asdodged_barwithgroups, and asstacked_barwithstack = TRUE, the default for a table or a matrix;histogram()ashist;bwplot()asbox;levelplot()asheat; andcontourplot()andlevelplot(contour = TRUE)ascontour.xyplot()is read by itstype: its points aspoint, one layer per group, named after it;"l"aslineand"s"or"S"asstep, one series per group;"h"aslollipop; and"r","smooth"and"spline"as asmoothcurve.densityplot()is read assmooth, one curve per group;dotplot()asdot, or as points when a level holds several values; andstripplot(),qqmath()andqq()aspoint, with the lines, spikes, fits and averages theirtypeadds read asxyplot()’s are – save a fit over a factor axis, below – and a line through the levels of a horizontal dot or strip plot read level by level, as its vertical transpose is. Ahistogram()of a factor, one bar per level, is read as abarlayer named by its levels.xyplot()of a time series is read as aline, in a panel per series or superposed, its values named after the series rather than lattice’s ownx. Every layer names the marks it reads, so each point, bar, bin, dot and spike, each box, each heat map cell and each line, curve and contour is highlighted as it is read.The groups of a line, step or smooth layer that share no x value – the densities of a grouped
densityplot(), say – are read as a layer each, named after the group, which Page Up and Page Down reach. Kept together, only the first could be reached: the frontend moves Up and Down only between series that meet at the x being read. Where a panel holds layers of more than one kind, a group’s layers also say what they are, as in “4 (point)” and “4 (line)”, and two curves of one kind say which curve each is: “4 (line)” and “4 (average)”, “4 (loess)” and “4 (spline)”, or “line” and “average” where the groups are read together.A conditioned chart (
y ~ x | g) is read one panel at a time, each panel a subplot titled by its strip and laid out as lattice lays the panels out; a strip made bystrip.custom()– itsfactor.levels, and the variable’s name wherestrip.names = TRUEdraws it – and anauto.key’stextname the panels and groups as they show them. A chart laid out over several pages is read from its first page – or the page apacket.panelpicks – with a warning that nameslayout =as the way to fit every panel on one, and so is its image when it cannot be read.mainis the chart’s title andsubits subtitle, and an axis is named byxlaborylab, or by the variable lattice would name it after.A chart the reading does not cover is shown as a static image rather than read wrongly:
cloud(),wireframe(),splom(),parallelplot(), a panel function of the user’s own or one such aspanel.violin,levelplot(useRaster = TRUE), latticeExtra layers and compositions (+ layer(),c(),doubleYScale()), a fit (type = "r","smooth"or"spline") over a factor axis, which lattice draws between the levels, where the axis names nothing, and a panel that fails to draw. Printed at the console, such a chart is drawn by lattice as before.Printing goes through lattice’s own
print.functionoption, which maidr sets once ‘lattice’ is loaded andmaidr_off()restores. A print that shares its page with other charts (split,position,more = TRUE,newpage = FALSE, given toprint()or carried in the chart’splot.args), a print into a file device such aspdf()orpng(), a print inside Shiny, andplot(p)are drawn by lattice as before. A chart lattice draws at the console while a Base R chart waits forshow()goes on a screen, with the theme set bytrellis.par.set(), not onto the hidden device maidr records that chart on. In R Markdown and Quarto a chart the chunk returns and one it draws withprint(p)are both made accessible, each in place of its own figure.options(maidr.lattice = FALSE)leaves lattice printing alone, and setting it back toTRUEtakes effect at the next print; see?"maidr-options". Saving, showing or knitting a chart leaves lattice’s own record of the chart it drew last – the onetrellis.focus()andtrellis.last.object()act on – as it was.An
xyplot(precision ~ recall, type = "l")– or one whose axes are otherwise titledRecallandPrecision– of values that are all fractions of one is emitted as apr_curvelayer, one curve per group, as a ggplot2 line ofrecallagainstprecisionis: a reader hears each point against the curve’s average precision and the best-F1 point in the description, where a line said the rates and nothing else. Any other line keeps its reading. Needs maidr.js 4.14.0 or later, which this release bundles.
webR
-
show()works under webR, where it used to stop atutils::browseURL(). The chart is added to the page the R session runs in, as an iframe in the element with idmaidr-output, or at the end of<body>when there is none; the frame is sized to the chart and Shift+Tab leaves it, as from any other maidr frame. A page can take the document itself by definingglobalThis.maidrWebRShow(html), and R code can by settingoptions(maidr.webr_display = function(html) ...); either is used instead of the iframe. R runs in a web worker, so reaching the page needs webR’sSharedArrayBufferor service worker channel, and a page that allowsunsafe-eval. Where the page cannot be reached,show()says why and saves the document to a temporary file; take it from R with the option instead, which works over every channel. webR’s own repository carries an older maidr until a CRAN release containing this is picked up by it; install the development version from r-universe (xability.r-universe.dev), which builds it for webR, or build it withrwasm::add_pkg(). Seevignette("webr"). A new CI job builds maidr for webR and, in a browser, installs it from that build and callsshow(). A ggplot2 bar chart, a faceted scatter plot, a patchwork, latticexyplot()andbarchart(), and Base Rbarplot(),plot(),boxplot()and a two-panel layout each become a document in webR that a keyboard reads in the page; the ggplot2 bar chart has been through the whole path in a browser. A plotly, highcharter or echarts4r widget given toshow()is made accessible withmaidr_htmlwidget()and shown the same way; its document carries the chart library, three to six megabytes. A plotly bar chart has been through the whole path in a browser.show(as_widget = TRUE)and the Shiny functions have not been tried.
Base R
- Added directed graph support: igraph’s own
plot()of a directed graph is emitted as adirected_graphlayer, so a reader walks the graph node by node and hears what feeds each node and what it feeds, with that node’s circle outlined. The nodes and edges are read from the igraph object, and each vertex’s own attributes are announced with it. It used to be emitted as a scatter with no points in it; an undirected graph, or one whose vertices are not all circles, is now shown as a picture instead. Needs maidr.js 4.14.0 or later, which this release bundles. - A
plot(recall, precision, type = "l")ortype = "s"– or any line or staircase whose axes are titledRecallandPrecision– of values that are all fractions of one is emitted as apr_curvelayer, as a ggplot2 line ofrecallagainstprecisionis, rather than a line or a step. Anabline()drawn over it, such as the chance level, stays a line. Needs maidr.js 4.14.0 or later, which this release bundles. -
plot()of a ROCRperformanceobject is read from the object. ROCR’s plot method draws its curve from inside ROCR’s namespace, where maidr records nothing, so the chart used to be emitted as a scatter with no points in it. A precision-recall curve,plot(performance(pred, "prec", "rec")), which ROCR titlesRecallandPrecision, is now apr_curvelayer, each point announced with the cutoff it was scored at, from the object’salpha.values; any other measure ROCR plots, a ROC curve’s rates among them, is a line. A cross-validated object of several runs is one curve per run. A plot drawn withavg,colorize = TRUE,downsampling,add = TRUEor atypeother than"l"is shown as a picture. ROCR exports an S4 generic forplot(), so attached after maidr it masks maidr’splot()and no bareplot()call is recorded at all: attach ROCR before maidr, or callmaidr::plot(). maidr now says so when ROCR is attached after it, and in the “No Base R plots detected” error. - A
wordcloud::wordcloud()chart highlights the word being read: as the arrow keys move, the word announced is recoloured in the reader’s highlight colour. Each term is matched to the text R drew it as by its label, so the layout’s random order does not matter. A cloud with a termwordcloud()could not fit on the page is read without a highlight rather than with highlights on the wrong words (#356).
Monarch tactile display
- A chart’s frame now delegates WebHID as well as Web Bluetooth and Web Serial (
allow="bluetooth; serial; hid"), so a Monarch in its Braille Terminal can draw an R chart inside a cross-origin frame, as a Dot Pad already could. Drawing on a Monarch also needs a maidr build that supports it.
ggplot2
- Added precision-recall curve support: a precision-recall curve is emitted as a
pr_curvelayer, one curve per classifier, so a reader hears each point’s recall and precision, its threshold and how far its precision sits above the share of positives, and the average precision of each curve and the point with the best F1 score in the description.maidr_pr_curve()declares one – it isgeom_path()with athresholdaesthetic andprevalenceandaparguments – andautoplot()of ayardstick::pr_curve()is read as it stands, by therecallandprecisionit maps. The trace needs maidr.js 4.14.0 or later, which this release bundles; an older bundle keeps the line reading these charts had. - Added percentile band support: ggdist’s
stat_lineribbon(), and ageom_lineribbon()of amedian_qi()summary, are emitted as apercentile_bandlayer, so a reader enters each x on the median and walks up and down through the quantiles, each announced with the band it bounds (“Middle 80% is 0.4 to 1.61”), and each band is outlined as it is read. The chart used to fall back to a static image, since ggdist’s geom was not read at all. Only a median with quantile intervals is read this way – a ribbon of widthwspans the quantiles(1 - w) / 2and(1 + w) / 2– so a mean, a highest-density interval or several series in one layer keep the reading they had. Needs maidr.js 4.14.0 or later, which this release bundles. - A
stat_summary(geom = "ribbon", fun.data = median_hilow)band is now read as apercentile_bandlayer too: the stat computes the quantiles(1 - w) / 2and(1 + w) / 2around the median,wbeingfun.args$conf.int(0.95 unless set), so a reader enters each x on the median and hears the band as the share it covers (“Middle 50% is …”) where it used to hear two bare bounds of an error bar. Astat_summary()line of the median (fun = median, orfun.data = median_hilow) drawn on the same rows is read as the band’s median and outlined with it, rather than a second line. Any other summary, a ribbon of several series or a flipped one, and a median line that sits on more than one band, keep the reading they had. Amedian_hilowpoint range or error bar stays an error bar: the band trace outlines one shape across every x, not one per- Needs maidr.js 4.14.0 or later, which this release bundles.
- Added directed graph support: a
ggraph::ggraph()drawing of a directed graph is emitted as adirected_graphlayer, so a reader walks the graph node by node and hears what feeds each node, what it feeds, and where the graph branches and merges, with that node’s point outlined. The nodes and edges are read from the igraph object ggraph laid out, and each node’s own attributes are announced with it. The chart used to fall back to a static image, since ggraph’s edge geoms were not read. An undirected graph keeps the reading it had. Needs maidr.js 4.14.0 or later, which this release bundles.
plotly, highcharter and echarts4r widgets
-
maidr_htmlwidget()takespercentile_bands, which declares the fan charts an echarts4r chart draws – a mediane_line()and, for each band, ane_area()of its width stacked on an invisible line at its lower edge – naming the median’s series, each band’s filled series and the quantiles of its edges. MAIDR then reads the fan as onepercentile_bandlayer, as it reads ggdist’s lineribbon: a reader enters each x on the median and walks through the quantiles, and each band is outlined as it is read. ECharts carries nothing that says which quantiles a band’s edges are, so without the declaration the series stay a line and two areas. The levels are checked in R, with the rules maidr.js applies – fractions, everylowerbelow 0.5 and everyupperabove it, the bands nested – and a series name the chart does not have is a warning. It is passed to the ECharts adapter’spercentileBandsoption, which maidr.js reads from 4.15.0; the maidr.js bundled with this release is 4.14.0, so the option takes effect withuse_cdn = TRUEand is otherwise checked, then ignored with a warning, until the bundle is raised. Draw the fan over a category x axis: over a numeric one ECharts stacks the series on the x values, and draws no band.
Performance
- The SVG export, the maidr-data payload and the ggplot2 heatmap grid now do their per-shape and per-point work in C++ through ‘Rcpp’, which is added to Imports and LinkingTo; the package now has compiled code. The output is byte for byte what it was. Measured with
save_html(): a 300 x 300geom_tile()heatmap went from 65 s to 5.6 s – its cells were found by scanning every source row for every tile – a 100 x 100 one from 2.2 s to 0.7 s, a 50,000-pointgeom_line()from 2.7 s to 1.2 s, a 50,000-pointgeom_point()from 4.4 s to 3.5 s, and a 2,000-bargeom_col()from 3.5 s to 1.9 s (#343).
Bug Fixes
R Markdown and Quarto
- A Quarto document holding a chart made with
maidr_htmlwidget()renders again. Quarto copies every dependency of a page from disk, and stopped with “Dependency maidr-locale-config 1.0.0 is not disk-based”, or the same ofmaidr-dotpad-configwhen a DotPad SDK location is set. The two dependencies, which only write to the page’s<head>, now name the bundle’s directory without declaring any file in it (#352). - A Base R chart in an R Markdown or Quarto document is read from the chunk’s own graphics device. maidr read whichever device was current when ‘knitr’ wrote the figure, which is the chunk’s only when no other device is open: with a
pdf()left open, or the IDE’s screen when the document is rendered from the console, a chunk’s Base R chart came out as a static image, and a chart drawn at the console and never shown could be shown in place of the figure of a chunk that drew no Base R chart (#352). - In Markdown output (
github_document,md_document, a plainknitr::knit()of an.Rmd) a chart is drawn by its library as one of knitr’s figures. It used to be an iframe, which GitHub drops, and online ‘rmarkdown’ stopped the render, refusing the HTML dependency the frame brought (“Functions that produce HTML output found in document targeting …”) (#352). - maidr’s dialogs, opened from a chart a knitted page shows inline (new in this version, above), are enlarged only by as much as maidr.js left their text short of the size it has on a page that leaves its root font size alone, which follows the reader’s default font size. On a page whose root is smaller –
html_document‘s default theme, the gitbook format of ’bookdown’, ‘flexdashboard’ – they were zoomed by 16px divided by the root’s size (1.6 on a 10px root), whatever size maidr.js had given their text and whatever the reader’s default, so a maidr.js that sizes its dialogs for such a page itself would have shown 22.4px body text under a 20px title. With the bundled maidr.js 4.13.0 their body text is still 14px under a 20px title at a browser’s default font size (#354).
ggplot2
- A layer that maps x or y in its own
aes()is read under the axis title,labs()included, as the chart shows it. The layer’s own mapping replaced the title outright, sogeom_histogram(aes(y = after_stat(density))) + labs(y = "Density")was announced as “after_stat(density) is 0.07” while the axis said “Density”, and a one-layergeom_col(aes(month, sales))lost itslabs()titles to “month” and “sales”. A layer is now named for its own mapping only when another layer read beside it plots something else on that axis –geom_col(aes(y = sales)) + geom_line(aes(y = target))still reads “sales” and “target” – and then by the name ggplot2 gives it, “density” rather than “after_stat(density)”. Decoration maidr does not read, such asannotate()andgeom_text()value labels, does not count as such a layer, nor does astat_function()curve, so a density histogram with a normal curve over it reads “Density” on both. Layers read together as one entry – line layers merged into one multi-series layer, and the layers of a facet panel – are named by the axis title when they plot different things, rather than after the first of them (#349). - A point layer follows the same rule. It was always read under the axis title, so the points of
geom_col(aes(y = sales)) + geom_point(aes(y = target))were announced as “sales”, and twogeom_point()layers plottinghpandqsecboth as “hp”. They now read “target”, and “hp” and “qsec”; so does ageom_pointrange()orgeom_errorbar()that maps its own y beside a layer plotting something else. A constant inaes()– they = 0a lollipop’s stems start from, oraes(x = "")for a one-group strip chart – names no variable, so it neither names its layer (“0”) nor counts as another layer plotting something else; nor does a segment or curve drawn with an arrow. A category dodged or nudged by hand –as.numeric(term) + 0.1, orfactor(cyl)besidecyl– is still that category (#349).
Base R
- A Base R chart that draws random numbers is exported as it was drawn. maidr exports a chart by drawing the recorded call again, and drew it from whatever random state the session had by then: a
wordcloud()came out with its words turned and placed differently from the cloud on the reader’s screen, astripchart(method = "jitter")with other jitter, and each again differently on every save. Each call now keeps the random state it started from and is drawn again from it. Exporting a chart also no longer moves the session’s random numbers on, which changed what a script’sset.seed()gave every call after it. - A Base R chart is laid out on a page of the size it is drawn at. maidr drew it with
ggplotify::as.grob(), which lays every drawing out on a 7 x 7 in page of its own, and the chart was then stretched onto its 7 x 5 in: a legend’s lines ran into one another and the title sat half as far below the top edge as R puts it. The drawing is now made on a page of the chart’s own size, with the graphical parametersas.grob()sets, so at 7 x 7 in it is the drawingas.grob()made (#355). - A Base R chart is drawn with the margins and text size its
par()calls set –mar,mai,oma,omi,mexandcex– where maidr drew it with R’s own. A chart given small margins, aspar(mfrow = c(5, 1), mar = c(1, 2, 1, 1))gives a grid R draws at 7 x 5 in, was drawn with larger ones and its plots squeezed, and at a size asked for it was called too small to draw, though R drew it there (#355). - A Base R chart’s axes show the tick labels R shows. R’s
axis()leaves out a label that would run into the one before it, but the drawing maidr makes of a Base R chart kept every label, so they ran together on a small chart or in the panels of apar(mfrow)grid: 10, 12 and 14 on a short y axis where R shows 10 and 14. The labels R leaves out are now left out too, measured as R measures them at the chart’s size (#355). - A Base R
layout()page is drawn with thewidths,heightsandrespectits call sets. maidr set the page up again from the matrix alone, so every column was as wide and every row as tall as the others: the first plot oflayout(matrix(1:2, 1), widths = c(3, 1))took half the width, where R gives it three quarters, and the plot of a single cell, as inlayout(matrix(1), widths = lcm(5), heights = lcm(5)), filled the page. Each plot is now drawn where R draws it – withlcm()sizes, arespectmatrix, cells that span several rows or columns, empty cells, and in the one cell of each layout set up over a page of plots, a grid’s among them, to draw a plot or a legend’splot.new()panel over them, or in the one panel of such a layout over several cells givenwidths,heightsorrespect– and shows the tick labels R shows at its size. A size asked for now stops when R cannot draw the page at it, and a size no one asked for is enlarged until every plot, the smallest included, has room. When it is the cells the call sized withlcm()that do not fit on the page, the error and the message name them. The matrix is the argument R takes for it, written by name after the sizes, as inlayout(widths = c(3, 1), mat = m), or without a name after a size written with one, as inlayout(widths = c(3, 1), m). maidr took the widths for the matrix: it warned that it could not draw the page, drew none of it, and read the plots in a grid of the wrong shape, one of them missing. A size left out with an empty argument, as inlayout(m, , c(1, 3)), which leaves out the widths, no longer loses the page either: maidr drew only the first plot, over the whole page, and read every plot as one. Alayout()maidr did not record, made throughgraphics::layout()or before an earliershow()orsave_html()on the device, keeps the widths and heights R gave its cells too, read from where R drew its plots, where every edge of its cells is an edge of one and its cells fill the page, where maidr drew its columns all as wide, and its rows all as tall, as each other. Its plots are read in the grid of those cells too, as a recordedlayout()’s are, so a reader moves through other subplots than before: maidr read the plots ofgraphics::layout(matrix(c(2, 1), 1), widths = c(1, 3)), or of a scatter plot with marginal plots laid out withwidthsandheights, as one subplot of several layers, and a plot spanning a row of cells over rows of other heights as a subplot in the first of them only. Each plot is now a subplot of the cell R drew it in, in each cell it spans, and a cell no plot was drawn in is an empty subplot. Such a layout’s cells keep the share of the page R gave them on the device the plots were drawn on, which is their size on a page of any size wherewidthsandheightsset it, but not wherelcm()did, and R does not say which. A size no one asked for is enlarged until those shares leave every plot room, as for a recordedlayout(), and the message says it is the shares: anlcm(4)column drawn on a 10 x 8 in device is then drawn at 9 x 5 in, where R draws it 4 cm wide at 7 x 5 in. At a size asked for that they leave a plot no room at, the cells are drawn the same size, as before, rather than the chart stopping, both where R draws the page, as for thatlcm(4)column at 7 x 5 in, and where R stops, as for relativewidthsof 5 : 1 there. The one cell of such a layout is read as R reports it, a region of the page aspar(fig = )gives one, so a cell sized withlcm()keeps its share of the author’s page: drawn smaller than that page, the chart can stop where R draws it. A grid set up without maidr after a recordedlayout()of its shape, aswithr::with_par(list(mfrow = c(1, 2)), ...)afterlayout(matrix(1:2, 1), widths = c(1, 3)), is not given the recorded call’s sizes: maidr reads the regions R drew the page’s plots in, which are the recorded call’s cells only where R drew them in that layout (#361). -
save_html()of a lattice chart exports that chart even while a Base R call is recorded on the current device. The Base R adapter claimed any object once the device held a recorded call, so the chart was written out as the recorded Base R chart instead. The adapter now leaves a ggplot2 or lattice object to that package’s reading, whatever order the plotting systems were registered in (#333). -
plot()of a lattice or ggplot2 object is no longer recorded as a Base R chart. maidr’splot()wrapper took the call for one: with no graphics device open it drew the chart on the hidden device Base R recording uses, so nothing appeared, and a latershow()opened an empty scatter plot. The call now passes straight to the package’s ownplot()method (#333). -
layout(1), andpar(op)with the settingsop <- par(mfrow = ...)saved, are read as the resets they are: a chart drawn after one is read as a single panel. maidr kept the grid set before the reset, and described the chart as one panel of a grid whose other panel was empty (#352). - A Base R chart is titled the way R titles it.
hist(mtcars$mpg)writes “mtcars$mpg" under its x axis and "Histogram of mtcars$mpg” above it, and the chart maidr drew, inshow(),save_html(), a knitted document and the static image a chart falls back to, wrote the values instead, “c(21, 21, 22.8, 21.4, …)” for as long as the data ran: maidr draws the chart again from the values it recorded, and R names these titles after how an argument was written. The same went for the other charts R titles that way, among themplot(x, y),plot()of a vector, a time series or a table,image(),persp(),matplot(),mosaicplot(),sunflowerplot(),spineplot(),cdplot(),qqplot(),acf(),pacf(),ccf(),interaction.plot(),cpgram(),monthplot(),lag.plot()andsymbols(), and for two arguments written alike, as inplot(rnorm(100), rnorm(100)). The chart is still drawn from the recorded values, so each chart a loop draws, or one drawn fromrnorm(), shows the data it was drawn with. An argument written longer than R lets a name be (10,000 bytes), or written as..1, is still titled with its values (#353). - The data a screen reader reads names a Base R chart’s axes, and a histogram’s title, as the chart draws them.
hist(mtcars$mpg)was read as a histogram over “Bin” with no title, andplot(x, y)andqqplot(x, y)with no axis names at all, because the recorded values no longer said how the arguments were written. The call now keeps that text, so a histogram is read as “Histogram of mtcars$mpg" over "mtcars$mpg”,plot(v)as “Index” against “v”, and the lines and density curves drawn over either chart name the same axes. A title the call gives, or blanks withmain = NULL, still wins (#353). - A Base R chart titled with a plotmath call,
main = bquote(mu == .(n))orxlab = quote(x[i]), keeps its drawing and its data. maidr handed the recorded call on to be evaluated, so the chart maidr exported had nothing drawn on it, a box plot or Q-Q plot was read with no data, an axis was named “[” in the data, andbarplot()stopped at the call itself with “object ‘mu’ not found”. The call is now recorded as the expression it stands for, which R draws the same (#353). - A Base R chart whose title or axis title is not one string is drawn as R draws it.
i <- 3; plot(1:5, main = i)andbarplot(c(1, 2, 3), xlab = 2024)were exported with nothing drawn on them, and nothing said so: the drawing maidr makes of a Base R chart again stopped on amain,sub,xlaborylabgiven as a number, a logical, several values or a list such aslist("Title", font = 2), or as plotmath tocurve(),ylab = quote(x^2), or to a formulaplot(),main = expression(alpha); on atitle()given severallineoroutervalues, none, or a missingouter; on anaxis()given several values ofside,tick,pos,outer,font,lwdorlwd.ticksor a missingouter,lwdorlwd.ticks, onaxis(labels = NA)andaxis(1, at = numeric(0)); and on anmtext()spread over several sides,adjorpadjvalues, such asmtext(c("Left", "Right"), side = c(2, 4)). It left out values of anmtext()whoseatheld fewer positions than values, or a missing one, drew a missingmtext()text or tick label as “NA”, and drew an axis’s ticks in each of severalcol,col.ticksorltyvalues in turn. Each is now drawn as R draws it, several values ofmain,xlaborylaba line apart and those ofsuball on its one line, and the title and axis titles a screen reader reads are the same text, a line for each value: they were the first value alone. The titles ofcurve(), and of a formula plot with asubset, written as code, such asmain = grpin a loop, are read the same way, and drawn with the values R drew them with: they were announced as the first word of the code, or as no title, and drawn by running the code again, so that a title holdingsample()or a counter was drawn with another value. The y axis of aplot()of several time series, drawn a panel each, is announced as the first series’ name, the title R gives its panel; it was theylabR does not draw. A chart that still cannot be drawn again, such as one withaxis(1, padj = c(0, 1)), falls back to the static image, with a warning that says why, rather than being exported empty; the image’s alt text says it could not be made interactive, and names the page it shows, the last one R drew: by the chart’s title, or for several panels by the title R drew over them or by each panel’s own, its plot’s ortitle()’s. In a knitted document it stays knitr’s figure, and the document’s build says why once, as it does for a chart whose build stops (#358). -
plot()of a function, such asplot(sin, -pi, pi),plot(dnorm, -3, 3)orplot(function(x) x^2), is read as the linecurve()draws for it, over the points R drew, wherever the function is written among the arguments, as inplot(main = "Sine", sin, -pi, pi).save_html()andshow()stopped with “object of type ‘builtin’ is not subsettable” (or ‘closure’ or ‘special’), a knitted chunk showed a picture of it, andplot(sin)with no range, or with onlyfrom,toorxlim, was read as a scatter with no points. Its axes are named as R names them, “x” and the first line of the function as written, “sin”. Drawn over another chart withadd = TRUE, or as points withtype = "p", it is shown as a picture of the chart, ascurve()is. A function ofTRUEandFALSE, such asplot(function(x) x > 0.5), is read at the 0 and 1 R draws it at, and so iscurve(x > 0.5), which was read as a line with no points; one whose values are other than numbers, such as dates, is shown as a picture.plot()of a time series of one series, a one-way table or a data frame of two columns is announced with the axis titles R draws for it, such as “Time” and “AirPassengers” forplot(AirPassengers), where it had none, or had them only with another argument written first, as inplot(main = "Nile", Nile). So written, a time series is read as the line R draws for it andplot(main = "D", density(x))as the density curve, as they are written first, where each was read as a scatter of its points. A titleplot()derives for an axis, such as “Time”, “v” forplot(v), “mpg” forplot(mpg ~ wt, data = mtcars)or “Density” forplot(density(x)), is not announced where R draws none: where the call blanks it, asylab = ""does, or gives it asNULLto a time series, or turns titles off withann = FALSEorpar(ann = FALSE).plot(v, ylab = "")was announced with “v”. A title written on an axis after the plot withtitle(xlab = )orylabis announced as R draws it there:plot(x, y, ann = FALSE); title(xlab = "Weight")was announced with “x”. So is one written withmtext()centred on that side, the string nearest the axis where there are several, where maidr announces no title of its own for that axis, as forplot(x, y, ann = FALSE). A bar chart, box plot or strip chart keeps maidr’s “Category” and “Value”, or a formula’s names, andhist(),curve()andqqnorm()keep the titles they derive, even where R draws none. On a chart of several y axes, drawn withpar(new = TRUE)andaxis(4), each series is titled by what is written beside its own axis (#359). - A Base R chart is the page R’s device shows: the last one. After
hist(mtcars$mpg); hist(mtcars$hp)R shows the second histogram alone, on a page of its own, butshow()andsave_html()made one chart of every plot drawn on the device: both histograms’ data in one subplot, under a drawing of the first, so a reader heard data that was not on the chart, and saw a chart R no longer showed. The same happened whenever a plot started a new page, asplot(x)and thenheatmap(m), or a fifth plot underpar(mfrow = c(2, 2))after a reset to one panel. maidr now reads the page each call was drawn on from R itself, so the chart is the last page, with all the panels of apar(mfrow),par(mfcol)orlayout()grid on it, each plot in the panel R drew it in – also when maidr did not record the call that set the grid up, made throughgraphics::par()orwithr::with_par(), or before an earliershow()orsave_html()on the device, or when a plot is drawn over the whole page afterpar(mfrow = c(1, 1), new = TRUE), as a legend for all the panels is, where maidr read every plot as a layer of one subplot. A plot drawn afterpar(new = TRUE)is drawn over the plot before it, in its panel, where maidr drew the first alone or gave the second a panel of its own; an inset drawn afterpar(fig = , new = TRUE), or a plot in a screen ofsplit.screen(), is drawn in the region of the page R gave it; a plotpar(mfg = )sends to a panel out of turn is in that panel;plot.new()andframe()take a panel as they do in R, where maidr moved the next plot into it; alegend(),text()orlines()drawn on such a panel, or on a plot maidr does not record such assmoothScatter(), is drawn there, where maidr drew it over the plot before and read it as part of that plot; one drawn over a plot afterpar(new = TRUE)andplot.new(), as a second series with an axis of its own is, is drawn in the coordinates it was drawn in, where maidr drew it in the plot’s, and is read with that plot also in an earlier panel or screenpar(mfg = )orscreen()sent R back to, where maidr read it with the plot drawn last; one drawn afterpar(mfg = ), orscreen(n, new = FALSE)ofsplit.screen(), sends R back to the panel or screen of an earlier plot is drawn on that plot and read with it, where maidr read it with the plot drawn last, and drew it on that plot or not at all – but not what R clips away: afterscreen(n, new = FALSE), untilaxis(),title(),mtext(),box()or a change ofxpdworks R’s clip out again, R clips what is drawn to the plot in the screen before and shows none of it, and maidr neither draws nor reads it; and nothing drawn on an earlier page – its data, titles,lines()orlegend(), or a size it would need – reaches the chart, even when the plot that started the new page was drawn whilemaidr_off()was in effect. A pagereplayPlot()puts back on a device that keeps a display list is the chart, with what was drawn on it whenrecordPlot()saved it, where maidr read the plots and calls drawn since with it.symbols()withoutadd = TRUEdraws a plot of its own, and is the plot of its page, shown as a picture since maidr does not read it, wheresymbols()alone gave a chart with nothing on it. A page that holds no plot maidr recorded, as afterhist(x); plot.new(),hist(x); plot.new(); text(0.5, 0.5, "note")orhist(x); smoothScatter(y), or a pagereplayPlot()puts back from a plot drawn whilemaidr_off()was in effect, is no longer exported as the histogram before it, or as a chart with nothing on it:show()andsave_html()stop and say the page holds no plot maidr recorded. So do they, and say why, for a line added to a plot an earliershow()orsave_html()read, or to a pagereplayPlot()puts back from before then: that call let go of what maidr recorded of it. A plot drawn over the one before it after apar(new = TRUE)made throughgraphics::par()orwithr::with_par()is titled as after one maidr records: on a chart of two y axes, each series by what is written beside its own axis, where every title went to the second series. A plot in a screen ofsplit.screen()is drawn with the margins and size of text R drew it with, thosescreen()puts back for that screen, and without the outer marginssplit.screen()takes away while its screens are in use, where maidr drew it with those set last in any screen, under outer margins R had taken away; and so is a plot whose margins or size of text apar()call made throughgraphics::par()orwithr::with_par()set, which maidr drew with R’s own. The picture maidr draws of a page it cannot read or draw again has each plot where R drew it too – over the plot before it, in its panel of a grid, or in its screen or region of the page – also where what put it there was not recorded: apar()call made throughgraphics::par()orwithr::with_par(),screen(), or a panelplot.new()took; and whatpar(mfg = )orscreen(n, new = FALSE)sent R back to add is drawn on the plot R added it to, clipped as R clips it. The picture drew a plot after such apar(new = TRUE)on a page of its own, which lost the plots before it, as on a chart of two y axes whose second axis maidr cannot draw again; drew one such apar(mfg = )sent out of turn, or one after a panelplot.new()took, in the next panel; drew every plot of a call that draws several, asplot()of a fitted model does, on one page, where R shows the last of them; and drew asplit.screen()page’s last plot alone in the first screen’s place, with R’s warning “calling par(new=TRUE) with no plot” (#360).
Documentation
- The README gains an “R Markdown and Quarto” section, and it, the getting-started vignette,
?maidr_onand the examples say thatlibrary(maidr)is enough in a document; the example articles no longer callmaidr_on()(#352). - A new examples article, “lattice Chart Examples”, shows every lattice reading on a small chart, each marked [experimental], and is listed under “Experimental plot families” on the examples hub and in the pkgdown Articles menu. The README gains a lattice table under “Experimental Plot Types” and says what printing a lattice chart does; the getting-started vignette shows lattice use, and it and
?maidrlist lattice’s readings as experimental. Five lattice example scripts ship with the package, listed byrun_example()and run withrun_example("<name>", type = "lattice")(#333).
maidr 0.5.0
CRAN release: 2026-09-29
New Features
plotly, highcharter and echarts4r widgets
-
maidr_htmlwidget()makes an interactive chart drawn by plotly (plot_ly(),ggplotly()), highcharter or echarts4r accessible, by attaching the MAIDR JavaScript adapter for the library that draws it (#332). The chart is read in the browser once it has been drawn, so it works wherever the widget does: the viewer,htmlwidgets::saveWidget(), R Markdown, Quarto and Shiny, where a re-rendered chart is read again. It is pipe-friendly (w |> maidr_htmlwidget()) and loads the bundled scripts unlessuse_cdn = TRUE. An echarts4r chart is switched to ECharts’ SVG renderer, which MAIDR needs to highlight the mark being read. The Highcharts and ECharts adapters are bundled besidemaidr.js, from the same verified npm release.
maidr.js from the CDN
- The CDN paths now load the latest published maidr.js, as the Python binding does, rather than the version bundled with the package. That is
show()andsave_html()withuse_cdn = TRUE, and the widget, knitr and Shiny paths, which use the CDN when they find the machine online. The first CDN document in an R session asks jsDelivr’s data API, then the npm registry, which version is the latest, within 3 seconds for both (maidr.cdn_timeoutorMAIDR_CDN_TIMEOUT, clamped to 0.1 to 30), and every document in the session names that exact version, so what a reader loads does not shift under jsDelivr’s week-long cache of the@latesttag. The answer is kept for the session, and so is a failure: offline or blocked, the lookup costs no error and is not retried on every render, and documents name the bundled version, as py-maidr’s do. An answer older than the bundled version is refused the same way.options(maidr.cdn_version = ...)orMAIDR_CDN_VERSIONpins it instead: a version ("4.9.0", or"v4.9.0"),"bundled"for the bundled version or"latest"for the@latesttag, the two tags without a lookup; the option wins over the variable, and anything else warns once and is ignored.use_cdn = FALSEstill loads the bundled copy and makes no version lookup. See?"maidr-options".
Languages other than English
- A document that loads the bundled maidr.js now reads charts in the reader’s language when maidr.js speaks it: Korean, Japanese, Chinese, Spanish, German, French, Italian or Hindi, besides English. maidr.js (4.8.0 and later) keeps each of those in a locale pack it fetches from beside itself, and this package does not bundle the packs (0.8 MB), so these documents stayed in English even online. Every
use_cdn = FALSEdocument, and every one that inlines the bundle, now declareswindow.maidrLocaleBaseUrlahead of maidr.js, pointing it at the packs of the bundled version on jsDelivr. English still needs no network; another language is fetched when the reader is online and stays English when they are not. A CDN document is unchanged: its packs are beside the copy it loads.options(maidr.locale_base_url = ...), orMAIDR_LOCALE_BASE_URL, names another place for the packs, and""orFALSEdeclares nothing. See?"maidr-options".
DotPad tactile display
- A DotPad tactile display can now be reached from a fully offline document without naming a server.
maidr_download_dotpad_sdk()fetches the SDK maidr.js is pinned to (about 14 MB, once, into a per-user cache, every file verified against its recorded size and digest), and from then onshow()andsave_html()copy it intolib/dotpad-sdk-<version>/beside everyuse_cdn = FALSEdocument and declare the globals with that relative path. The optionsmaidr.dotpad_sdk_urlandmaidr.dotpad_asset_base_urlstill name a copy served from elsewhere, and win over a downloaded one;maidr.dotpad_sdk_dir(orMAIDR_DOTPAD_SDK_DIR) moves the cache. The widget, knitr and Shiny paths keep using the URL options only: their charts live insrcdocframes, where a relative path has nothing to resolve against. - The SDK’s braille engine now comes from the vendor’s own repository. maidr.js served
liblouis.datafrom a fork because the vendor’s copy had its line endings rewritten by git, which broke every braille table and dropped the text line to grade 1; upstream has fixed that, and the next maidr.js release pins the repaired commit. - The SDK pin now moves with the maidr.js bundle instead of living in R code.
inst/dotpad-sdk.jsonis thedist/dotpad-sdk.jsonthe maidr npm package ships as the single source of truth for its own pin, and.github/scripts/fetch-maidr-bundle.shcopies it in with every bundle refresh. The pin is SDK 3.0.3, served fromxability/dotpad-sdk-guide: the vendor publishes 3.0.3 only as a zip archive, so that mirror carries the extracted files, byte-verified against the archive. The download cache moves todotpad-sdk/3.0.3and documents carrylib/dotpad-sdk-3.0.3/.
ggplot2
- Added ROC curve support: a receiver operating characteristic curve is emitted as a
roclayer, one curve per classifier, so a reader hears the true positive rate on the unit interval, each point’s threshold and height above the chance diagonal, and the area under each curve and the best operating point in the description.maidr_roc()declares one – it isgeom_path()with athresholdaesthetic and anaucargument – andpROC::ggroc()andautoplot()of ayardstick::roc_curve()are read as they stand, by thesensitivityandspecificitythey map. The trace needs maidr.js 4.9.0 or later (this release bundles 4.11.0); an older bundle keeps the line reading these charts had. - Added pie chart support: a
geom_col()/geom_bar()layer undercoord_polar("y")orcoord_radial(theta = "y")is emitted as apielayer, one navigable slice per wedge.coord_polar("x")and multi-ring charts keep their bar reading. - A pie layer now says where its ring begins and which way its wedges run (
startAngle, degrees clockwise from 12 o’clock, anddirection), read offcoord_polar()’sstartanddirection, orcoord_radial()’sarcandreverse, and off the way the stack was built. maidr.js walks every pie clockwise from that start, pans each slice to where it sits and names its clock position onp; a default ggplot2 pie, whose wedges are built from the top of the stack down, is declared counterclockwise so the walk is turned round to match the drawing. A bundle older than the one that reads the keys ignores them. - Added step plot support:
geom_step()is emitted as asteplayer, one point per sample, withstepDirection("hv","vh","mid"). An ordinal factor y carries its level name aslabel.stat_ecdf()is read as one staircase per group;geom_step()on other computed stats is not read. - Added 100% stacked bar support:
geom_bar(position = "fill")is emitted asstacked_normalized_bar, announcing each segment’s share rather than its count. - Added area chart support:
geom_area()is emitted asarea,stacked_areaor (position = "fill")stacked_normalized_area, one point per input row and one highlight per series. In stacked layersyis the series’ own value.geom_area(stat = "density")remains a smooth. - Added error bar support:
geom_errorbar(),geom_errorbarh(),geom_linerange(),geom_pointrange()andgeom_crossbar()are emitted aserror_barlayers in either orientation, each interval highlighted. A layer with no estimate aesthetic reports the centre of its span; a dodged layer emits one series per group. -
geom_ribbon()is read: from a zero baseline as anarea, otherwise as anerror_barband (no highlight).geom_smooth(se = TRUE)carries its confidence band asyMin/yMaxon each fitted point, in faceted plots too;geom_density()is not given a band. -
geom_function(),stat_function()andgeom_quantile()are read as smooth curves.stat_function()with a geom the smooth reader cannot draw no longer stopssave_html():geom = "point"reads as a scatter andgeom = "step"falls back to a static image. - Added contour support:
geom_contour()andgeom_density_2d()are emitted ascontourlayers, one navigable curve per piece with itslevel. The filled variants fall back to a static image. - Added gantt chart support:
geom_segment(),geom_curve()and a flatgeom_spoke()(angle = 0) are emitted asganttlayers when every segment spans one axis at a fixed position on the other. Both orientations work, a lane with several spans keeps them all, and unused levels survivedrop = FALSE. Segments that share no coordinate, and angled spokes, keep the static-image fallback. - New
maidr_gantt(): ageom_rect()layer whose author declares it a schedule is emitted as agantt, lanes named, each bar highlightable. Nothing else about the chart changes – the built data and both panel ranges are identical to the baregeom_rect()layer’s – and an undeclared rectangle layer keeps exactly the reading it has today. A lane is named by the single explicit tick drawn inside it and by its position otherwise;lane_axis = "x"reads the mirror image. Five structural rules for telling a rect-drawn gantt from a heatmap, a waterfall or a highlight were measured against eight charts and every one of them claimed a chart the issue forbids, which is why the author is asked instead. - Added
geom_hex()support: each hexagon is a navigable bin announcing its centre and count. ‘hexbin’ is now in Suggests. -
geom_raster()is read as the sameheatlayer asgeom_tile(); a raster is a single grob, so its cells are announced but not highlighted. -
geom_dotplot()is read as a histogram, one bin per stack, in bothbinaxisorientations. -
geom_polygon()is read as one closedlineseries per group (and persubgroup). -
geom_rug()is read as onepointlayer per marked side, each tick highlightable, with the axis bounds braille grid mode needs. - A point on a discrete axis carries its category name (
xLabel/yLabel) beside its numeric position, in faceted, dodged and jittered plots too.
Base R
-
quantmod::chartSeries()candlestick charts keep their volume panel. The paneladdVo()draws –chartSeries()’s default whenever the data has a Volume column – is read as a second, bar layer beside the candles, one bar per period, each highlighted as it is read. These charts fell back to a static image before. Another indicator (addSMA(),addMACD(), …) still falls back, with the advisory, which now namesaddVo()as the one that is read. The volume bars are clipped to their panel, as R draws them, rather than running on into the date labels. - Added Base R
pie()support, one navigable slice per wedge. Text grobs with anNAjustification are repaired sopie()exports through gridSVG. - A
pie()layer now says where its ring begins and which way it runs:init.angleis converted from degrees counterclockwise from 3 o’clock to thestartAnglemaidr.js reads (degrees clockwise from 12), andclockwise = FALSE, the default, is declared asdirection = "counterclockwise"so the walk, the audio pan and thepclock position follow the wedges as drawn rather than the other way round. - Added Base R step plot support:
plot()andlines()withtype = "s"or"S"are emitted assteplayers withstepDirection"hv"/"vh". - Added Base R 100% stacked bar support: a stacked
barplot()whose every column sums to 1 (barplot(prop.table(m, 2))) is emitted asstacked_normalized_bar. Columns summing to 100 and single-row matrices staystacked_bar. - Added Base R violin plot support:
vioplot::vioplot()is emitted as theviolin_box+violin_kdepairgeom_violin()produces, replayingsm::sm.density()with the caller’shandrange; each section highlights its own grob. A category with no spread is omitted. The formula interfacevioplot(y ~ g)is not read and falls back to a static image. - Added Base R
contour()andfilled.contour()support, read ascontourlayers fromgrDevices::contourLines()at each function’s own defaultnlevels. gridGraphics cannot emulate inline level labels, and a filled field exports no per-curve element, sofilled.contour()is announced and navigated but not highlighted. -
curve()is read as an interactive line, including when called inside a function that binds the expression’s variables. Draw types other than a polyline andcurve(add = TRUE)keep the static fallback. -
plot(type = "h")andlines(type = "h")are emitted as alollipoplayer. -
dotchart()with one value per category is read as a horizontaldotlayer; the grouped/matrix form still falls back. - Added Base R
mosaicplot()support, read as amosaiclayer: each cell carries its conditional proportion, its column’s share of the whole, its count and its fill level, with the second dimension named onz. Bothmosaicplot(table)andmosaicplot(~ a + b, data = )are read; a table of three or more dimensions, or a formula call withsubset, falls back to a static image. - Added Base R
spineplot()support, read as amosaiclayer likemosaicplot(). The drawn table is recovered by replaying the call off-screen, so a numericxannounces the interval bins the chart labels; every cell, including an empty one, is highlightable. - Added Base R
cdplot()support, read as astacked_normalized_arealayer. Bands come fromcdplot(plot = FALSE), trimmed to the drawn x range and listed bottom to top; a formula call’ssubsetis honoured. - Added Base R
assocplot()support: a Cohen-Friendly association plot is read as aheatlayer of one Pearson residual per cell, with axes named fromdimnames()andzlabelled “Pearson residual”. Tile widths are not announced. - Added Base R
fourfoldplot()support, conditional on the caller’s ownstd: understd = "ind.max"or"all.max"the four quadrants are the four counts – measured,radius^2 * max(count)recovers each cell exactly – so a 2x2 table is read as aheatgrid of one count per cell, one selector per quadrant, with the levels and axis names taken from the labelsfourfoldplot()itself draws (including itsRow/ColandA/Bdefaults). Partial spellings such asstd = "ind"are read. Under the defaultstd = "margins"the four radii carry one number, the odds ratio, so that call still falls back to a static image and now says why; a 2x2xk array falls back too. The confidence arcs are not announced. - Added Base R
stripchart()support, read as onepointlayer per group with the group name carried as the point label;group.names,at,vertical = TRUEandmethod = "jitter"are honoured. - Added Base R
qqnorm(),qqplot()andqqline()support. The quantile pairs come fromplot.it = FALSE, sodatax = TRUEandqqnorm()’s default title and axis labels are read as drawn;qqline()is read as alinelayer from its ownprobs,qtypeanddistribution.qqplot(conf.level = )still falls back to a static image. - Added Base R
bxp()support, read as the sameboxlayer asboxplot()from the summaries inz.xlab/ylabare used; otherwise the generic axis names apply. - Added Base R
pairs()support, read as a scatterplot matrix: onepointcell per off-diagonal panel. - Added Base R
acf(),pacf()andccf()support, read as alollipoplayer of one spike per lag, with the value axis named ACF / Partial ACF / CCF. - Added Base R
interaction.plot()support, read as a multi-serieslinechart of the cell means, one series per trace level. - Added Base R
monthplot()support, read as onelineseries per cycle position (named bymonth.abbfor a monthly series). Thebasereference segments are not announced. - Added Base R
lag.plot()support, read as a grid ofpointcells, one per series and lag. - Added Base R
stars()support, read as aradarlayer: one series per row and one spoke per column, announcing the caller’s values rather than the scaled radii. No highlight yet. - Added Base R
termplot()support, read as onelinecell per term on the last page drawn; factor terms are declined. - Added Base R
spectrum()andcpgram()support: alineover the spectral density and astepover the cumulative periodogram. The confidence and KS reference marks are not announced. - Added Base R
biplot()support, read as twopointcells, scores and loadings, each on its own axes. - Added Base R word cloud support:
wordcloud::wordcloud(words = , freq = )is read as aword_cloudlayer of terms and their counts, honouringmin.freqandmax.words. Attach ‘wordcloud’ before ‘maidr’ or callmaidr::wordcloud(), withwordsandfreqnamed or positional. No highlight.
Bug Fixes
Rendering and integration
A chart rendered with
use_cdn = FALSE, or offline, works again when R runs under a C locale (a container, a CI runner, many servers). The chart’s frame carries maidr.js inline in itssrcdocattribute, and the bundle’s non-ASCII characters travelled as raw bytes, which knitr’s output and an htmlwidget’s JSON rewrote under a C locale as<e2><80><a6>; the script no longer parsed, and the chart was a plain picture. Every non-ASCII character in the frame’s document is now a numeric character reference (…), which the browser decodes, so what goes into the page is ASCII whatever the locale.A self-contained R Markdown or Quarto document (
self_contained: true, R Markdown’s default, orembed-resources: true) rendered online now works offline. Each chart’s frame loaded maidr.js from the CDN through a<script src>inside itssrcdocattribute, where pandoc’s resource embedding cannot see it, so the document still needed the network and offline its charts were plain pictures. The knitted document now carries one copy of the bundle, embedded or in its_filesfolder like any other dependency, in a script no browser runs; a chart whose CDN load fails reads that copy from the page instead. The CDN is still tried first.The widget (
show(..., as_widget = TRUE), andrender_maidr()in Shiny) does the same. Its chart frame now falls back to a copy of the bundle on the widget’s page, so a self-contained document holding widgets works offline too, and so does a Shiny app whose readers cannot reach the CDN. The page already carried that copy, as a script it ran for nothing: each chart runs in its own frame, where the page’s copy could not reach it. It is now an inert script, shared with the knitted charts’ copy, and only a widget whose frame loads from the CDN adds it: a widget drawn withuse_cdn = FALSE, or offline, carries the bundle in its frame and puts no copy on the page, where every widget used to add 1.7 MB it never read. The widget no longer has anhtmlwidgetsyaml, so the bundle refresh rewritesMAIDR_VERSIONalone.Bar, histogram, scatter, dodged, stacked and normalized bar, pie, dot and lollipop layers highlight again with the bundled maidr.js 4.x, on ggplot2 and Base R alike. Every processor built
selectorswithlist(), so a single CSS selector reached the payload as a one-element JSON array; maidr.js 3.x read that as the string it held, and 4.0 changed the contract so an array names one selector per data point (or a per-series grid), resolved one element for seven bars, and dropped the layer’s highlight while navigation and speech kept working. The payload now carries a plain string for every layer type whose frontend model reads one selector for all of its marks, joined with", "when a processor names several containers, and a bar layer in a panel that holds a second bar layer now addresses its own rects rather than both layers’.Dodged, stacked and normalized bars, ggplot2 and Base R, highlight the bar being announced. maidr.js 4.0 also stopped inferring that a layer’s rects are drawn category by category: a layer that does not say
domMapping.order = "column"is paired with its rects series by series, so once the highlight came back it landed on the wrong bar – “a, 10, u” announced while the 55 bar was outlined. Every segmented layer now declares the order it is drawn in. A headless-browser smoke test in CI presses the arrow keys on each of these charts and fails when nothing changes colour or when the outlined bars do not rank the way the announced values do, which is the check the 4.0.0 bundle refresh did not have (#316).-
Every layer type was then driven through the bundled maidr.js in headless Chromium, and the charts that still drew no highlight, or drew it on the wrong mark, are fixed for the same reason: the shape the frontend reads changed with 4.0 and the emitters had not followed (#316).
- A ggplot2 gantt (
geom_segment()schedules,maidr_gantt()) threw inside the frontend and took the whole figure with it – no announcement at all. The frontend readsdata.pointsanddata.lanes; the layer emitted the lanes asdataand the names beside it. Base Rspectrum(),cpgram()andtermplot()threw the same way: theirdatawas a flat list of points where the line model reads one series per array. - Base R
spectrum(),cpgram()andtermplot()also addressed the<g>holding their curve rather than the polyline, so no marker could be placed;assocplot()addressed a container id without the.1gridSVG appends; aspineplot()listed its tiles in drawing order in a flat list thatquerySelectorAll()resolved in document order, so every tile after the first was outlined for another cell; amosaicplot()with an empty cell declined to address any tile at all. Each now emits the per-cell grid the segmented and heat models read,nullwhere nothing was drawn, andgeom_bin_2d()does the same for its empty bins. - A dodged or stacked bar inside a patchwork composition was drawn from its rows as given, while every other path reorders them first, so its declared drawing order matched the drawing only by luck. A pie inside a composition had no selectors at all:
coord_polar()fixes the aspect ratio, and patchwork then places the leaf under a name the panel walk dropped. A bar layer in a composition, whose panel carries no placeholder before its layers, could not find its own slot. -
geom_rug()is emitted as the frontend’s ownrugtrace (with the axis it marks asorientation), which pairs each tick with its own element and announces the observation; read as points, a<line>tick could never be outlined.
- A ggplot2 gantt (
An offline document (
use_cdn = FALSE) can reach a DotPad tactile display without the network. maidr.js does not bundle the DotPad SDK and imports it from jsDelivr the first time a DotPad connects; the new optionsmaidr.dotpad_sdk_urlandmaidr.dotpad_asset_base_url(or the environment variablesMAIDR_DOTPAD_SDK_URLandMAIDR_DOTPAD_ASSET_BASE_URL) point it at a copy you serve instead. They are written ahead ofmaidr.json every path that loads it:show(),save_html(), the widget, knitr and Shiny. Without them a DotPad needs network access on first connect, which the offline documentation now says (#304).A ggplot2 chart that maidr can read but cannot export gets its static picture. The fallback printed the chart through maidr’s own print method, which rebuilt it, failed again and opened a fresh
png()device on every round until R ran out of them.The Base R fallback picture no longer records its own replay, which left phantom layers on the next device R opened. A Base R chart that gridSVG cannot export (
matplot(matrix(1:12, 4)),symbols()) falls back to the static picture with a warning instead of stoppingsave_html();maidr_set_fallback(enabled = FALSE)re-raises the error.maidr_set_fallback(format = "svg")is honoured, andmaidr_set_fallback()keeps the settings it is not given.show(as_widget = TRUE)andmaidr_widget()accept Base R plots. Shiny’srender_maidr()renders Base R plots (plot(),barplot(),hist()) and renders nothing for a reactive that draws nothing. Recorded calls are cleared on the widget and Shiny paths.knitr:
maidr_off()disables RMarkdown interception and clears the recorded Base R calls, so a latermaidr_on()no longer replays them as phantom layers; PDF and LaTeX output use ggplot2’s own print method and knitr’s original plot hook; a secondmaidr_on()cannot capture maidr’s hook as the original.Tabbing out of a chart hands focus back to the page. The page checks that an element actually took focus (Shiny’s
display: contentswrappers refuse silently) and walks up to one that does, and in a Quartorevealjsdeck focus returns to the slide so the deck’s own keys work. Applies to the knitr,save_html()andmaidr_widget()paths.LaTeX in MAIDR’s AI chat responses is styled again: the bundle ships
maidr-math.cssbesidemaidr.js, with the embedded KaTeX fonts stripped to stay under CRAN’s size limit.inst/COPYRIGHTSlists the components that bundle embeds (D3 and Tone.js were listed and are not in it).CDN-versus-bundled auto-detection re-probes internet access every five minutes instead of once per session.
maidr-data JSON keeps full numeric precision (values were rounded to four decimals), iframe content is UTF-8 encoded on every locale, and plot ids no longer advance the RNG, so
set.seed()scripts stay reproducible.A non-ASCII label – a Korean or accented title, axis label or category name – reached the reader as
<ed><95><9c>under a C locale (a container, a CI runner, many servers): the chart’s document was passed throughenc2utf8()while carrying no encoding mark. It is now converted only when it says what it is, and escaped byte-wise.Charts are embedded with
srcdocrather than adata:URL, whose opaque origin has neither Web Bluetooth nor Web Serial whatever theallowattribute says, so a tactile display such as a Dot Pad can be reached from an R chart; the frame carriesallow="bluetooth; serial"for a chart inside a cross-origin frame. Reading by touch also needs a maidr build that supports the display; the bundled 4.11.0 does.save_html()andshow()no longer warn “number of items to replace is not a multiple of replacement length” on a chart with a rect of negative height or width, such asbarplot()with a bar below the baseline.A currency prefix other than
$resolves in every locale;label_dollar(prefix = "€")was announced as USD outside a UTF-8 session.The startup message says that ggplot2 plots open in the viewer automatically while Base R plots are recorded until
show()is called, and names the masking when ‘quantmod’ is attached after ‘maidr’.cancel_auto_show()removes its task callback by name, so it can no longer remove another package’s callback.show()hands an object that is not a plot – an S4 object, a vector – tomethods::show(), which attaching maidr masks, so it prints as it did before. It failed with “argument is of length zero” (#320).
ggplot2
- A
geom_line()orgeom_step()over a numeric x now emits x as a number ("x": 0) rather than a string ("x": "0"), matching thegeom_point()beside it, sogeom_point() + geom_line(aes(y = trend))carries the same x in both layers and the line’s x takes the axis format. A discrete x is still its category label and aDateorPOSIXctx still an ISO string. - A
geom_line()orgeom_step()with a missing y inside the series now announces that position as missing (y: null), where it dropped it, sox = 0:3, y = c(1, NA, 4, 5)reads four positions rather than three and the gap is heard. The Python binding and the base R line path already emit the null. Leading and trailing missing values, which ggplot2 does not draw (a moving average’s warm-up), are still left out. - Horizontal bar charts are read correctly:
geom_col()/geom_bar()withaes(y = category, x = value), alone or withposition = "dodge","stack"or"fill", andgeom_histogram(aes(y = ))emitorientation = "horz"with values, labels and order matching the drawn bars; they came out empty, mislabelled or unannounced. Reading such a chart no longer swaps the caller’s own layer mapping in place.coord_flip()is still reported"vert", which reads correctly. - Faceted plots keep the axis labels ggplot2 derives while building (such as “count”) and the legend title, and each panel keeps its layers’
orientationanddomMappinghints instead of falling back to defaults. - Faceted plots: a
position = "fill"bar announces proportions; a panel whose facet value isNAannounces its own rows (dodged bars, heat maps and stacked bars, which used to abort the export); an empty panel (drop = FALSE) carries no layers and no fabricated selector, for a smooth or a line as for the other geoms; a bar on a continuous,DateorPOSIXctaxis announces its own x; a line on a transformed x scale announces data values; box plot panels carry their own category names; heat map panels report their own cells; box plots, histograms, smooths, heat maps and stacked/dodged bars no longer fail with “unused arguments”. Faceted violins render but are not interactive. - ‘patchwork’: layer ids are unique across a composition; each leaf’s axis number formats and ggplot2-computed axis labels are kept; nested layouts (
(p1 | p2) / p3) emit working selectors for every layer type; a violin leaf emits its layers; a faceted leaf no longer displaces the plots after it; plots afterinset_element(),free()orwrap_elements()are described; a leaf whose processor errors is left silent instead of failing the composition; a horizontal bar leaf keeps itsorientationand a dodged count leaf itsdomMapping. - Transformed scales (
scale_x_log10(),scale_x_sqrt(),scale_x_reverse()): point, line and smooth layers announce the values the axis shows. A transformed axis emits its label but no navigation grid.coord_trans()is unchanged. - Curve layers highlight their own polyline: a
geom_smooth()orgeom_function()beside a latergeom_line(), a chart mixinggeom_line()andgeom_step(), a groupedgeom_line(aes(colour = g))beside ageom_smooth(), and a groupedgeom_smooth()orgeom_density()(now one series per group, named after it) each get one selector per series. When curves cannot be matched to series, no selector is emitted. - Dodged bars:
geom_bar(position = "dodge")with empty (x, fill) combinations emits a full grid (an absent count is0) so highlights land on the right bar;geom_col()cells the caller never supplied areNA; categories follow the plotted order rather than text order; expression aesthetics such asaes(fill = factor(cyl))work; a missingxorfillvalue keeps the column ggplot2 draws for it. -
geom_line()on unsorted data announces each point’s own x; a factor y ongeom_line()/geom_path()carries its level name aslabel; a multi-series line announces its legend title instead of “Group”. -
geom_point()emits only the rows ggplot2 drew (a missingxoryshifted every later highlight);geom_jitter(),position_jitter()andposition_jitterdodge()announce the observation rather than the displaced position; colour/group categories are announced again in faceted scatters, for a data column and for an expression such ascolour = factor(cyl)alike. - Histogram and smooth layers read their own layer’s built data in multi-layer plots; heat map axes follow factor level order and are named after the mapped columns or
labs()rather than “x” and “y”;geom_bin_2d()is read as the grid ggplot2 computed, each bin named by its range. - Violin plots: box statistics are read per group from
stat_boxplot, so dodged,coord_flip()and continuous-x violins announce their own quartiles;geom_violin(width = 0)no longer errors; a horizontalgeom_boxplot()orgeom_violin()is navigated bottom-up. -
geom_hline(),geom_vline(),geom_abline(),geom_label(),geom_blank()andannotate()layers are skipped likegeom_text(), so none of them drops the chart to a static image. A layer that drew nothing – a recognised geom given zero rows, or an unrecognised one whose stat computed no rows because a Suggests package such as ‘quantreg’ is missing – no longer reaches the schema or costs the chart its interactivity; a plot made only of empty layers still falls back. - A pie layer highlights its wedges on ggplot2 3.4.x too, two polar layers on one panel each get their own wedges, and a negative slice keeps its sign.
- A box plot is named by the label
scale_x_discrete(labels = )writes on the axis rather than by the raw level. - Histogram, stacked bar, dodged bar and smooth layers emit no selector when the grob lookup finds nothing, instead of guessing an element id.
Base R
- A line or step layer (
plot(type = "l"),lines(),matplot(),plot(type = "s")) over a numeric x now emits x as a number rather than a string, as the point layer beside it does.axis()labels and aDatex are still emitted as strings. - A positional argument reaches the description under the name R matched it to (
hist(x, 20),plot(x, y, "l")), a recorded flag is read as the drawing function reads it (barplot(horiz = 1),hist(freq = 0)), and a recorded argument is looked up by its exact name, sodotchart(v, xlab = ),monthplot(x, xlab = )andchartSeries()no longer read the label as the data (#292). -
plot(y ~ x, data = d)is read as the scatter it draws, from the model frame the recording keeps, with the axes named after the two variables;plot()andboxplot()formula calls withsubset =no longer fail with “object not found” or “..3 used in an incorrect context”; a formula recorded with a vectorsubsetreads only the rows drawn; and a formula is snapshotted at record time, so rebinding its variables beforeshow()does not change what is announced. Asubsetwritten as an expression (subset = dose == 0.5) is evaluated through the snapshot the recording keeps, soplot(),stripchart()andpairs()formula calls read the rows drawn; a formula call whose frame cannot be built, andplot(y ~ f)on a factor, fall back to a static image rather than exporting a chart with no layers. - A deferred plot call inside a loop (
plot(y ~ x, data = d, subset = grp == g),curve(f(x, k))) captures the values its expressions reference at call time, so each panel replays its own iteration. -
plot()of a matrix, data frame or list announces the axis grid it draws,plot()of a time series is read as a line over its own time index,matplot(m)emits one series per column, single-vectorplot(v)andlines(v)calls no longer error,lines(numeric(0))no longer aborts the render,plot(type = "n")is declined rather than announced, and multi-series selectors sort numerically. -
barplot(): a matrix withoutbesideis read as stacked;horiz = TRUEemitsorientation = "horz"for plain, stacked and dodged bars;legend.textno longer adds legend swatches to the selectors; bar data is emitted in drawn order;heightis read from its own slot, sobarplot(beside = TRUE, height = m)is a dodged bar chart. A named vector is still sorted alphabetically before drawing, and the sorted arguments are what is recorded. -
abline()spans the axisplot()set up – the data extended 4% each way, or an explicitxlim/ylim– rather than 5% beyond the data;spineplot(x, y)on bare vectors names its axes after the variables rather than their written-out values;spectrum()andcpgram()in a laterpar(mfrow)panel highlight their own panel’s curve. -
hist()honoursright,include.lowestandnclass, and density histograms announce densities;hist(),boxplot()andbarplot()withplot = FALSEandqqnorm(plot.it = FALSE)are not recorded. -
heatmap()follows the dendrogram order it draws, labels unnamed axes with the original indices it prints, honourslabRow/labCol, and is no longer described upside down underrevC = TRUE;image()no longer transposes rows and columns; both accept a positional matrix, and cell highlights are no longer mirrored. - Box plots: outlier highlights follow the drawn order, and a box after an outlier-free box highlights its own outliers.
- Multi-panel figures:
layout()grids are multi-panel, a panel spanning several cells carries the panel in each, a trailingpar(mfrow = c(1, 1))no longer collapses the grid, plots beyond the grid follow R’s new-page behaviour, plots drawn before the layout call are excluded, highlights follow the panel actually drawn, per-panelaxis()formats stay per panel, and an unsupported overlay (segments(),arrows(),rect(),polygon()) silences only its own panel, with a warning naming it. Single-panel figures still fall back whole, and the static image of a multi-panel figure keeps its grid. -
pie(),barplot(),hist(),boxplot()andheatmap()drawn withoutxlab/ylabannounce default axis titles;main/subare matched exactly (subsetwas read as a subtitle) and tolerate non-character values;main = expression()no longer fails the save. -
stem()is no longer recorded as a chart.acf(),pacf(),ccf(),cpgram(),spectrum(),monthplot(),termplot(),lag.plot(),biplot(),bxp(),stars()andinteraction.plot()are recorded and exported, so a bare call is read rather thansave_html()reporting “No Base R plots detected”;persp(),sunflowerplot()andfourfoldplot()are recorded and fall back to a static image instead. -
chartSeries()charts are titled as R titles them. A chart given nonamewas titled with its series’ prices printed end to end, because the replay passed the recorded data where quantmod reads the expression it was written as; the date-range header was placed off the right of the page; and the right axis’s line and ticks were drawn through the middle of the plot. The title is recorded from the call, the header is placed inside the page, and the misplaced axis ticks are dropped with the line, keeping the price labels. -
chartSeries(): attaching ‘quantmod’ after ‘maidr’ masks maidr’s wrapper, which is now reported at attach time and in the “No Base R plots detected” error, withmaidr::chartSeries()as the explicit alternative;maidr::chartSeries(x, TA = NULL)no longer fails when ‘quantmod’ is loaded but not attached; the replay always uses the owning namespace’s function rather than maidr’s wrapper. - A wrapped Base R call returns with the visibility of the original, so
par("mar")andhist(x, plot = FALSE)print their value again. - The native-device fallback replays
par()andlayout()calls in their original order and strips maidr’s internal arguments. -
library(vioplot)orlibrary(wordcloud)afterlibrary(maidr)says, as quantmod already did, that the package now masks maidr’s wrapper on the search path and that a barevioplot()orwordcloud()call goes unrecorded; the “No Base R plots detected” error names it too, and the advice is to attach the package first or callmaidr::vioplot()explicitly (#320).
Enhancements
- Bundled MAIDR.js updated from 3.72.1 to 4.11.0. CDN documents name the version resolved once per session rather than
@latest(see “maidr.js from the CDN” above). - maidr now requires R >= 4.0.0.
- Iframe height auto-resize works in RMarkdown documents, not only in the htmlwidgets binding.
Documentation
- Help pages render the roxygen markdown they were written in, the internal R6 reference pages no longer carry stray keyword entries, and
R CMD checkno longer NOTEs “Lost braces” in four of them. Ten method descriptions onSystemAdapterandGgplot2ViolinLayerProcessorthat had been glued onto the previous method’s section have their own, andfind_graphics_plot_grobhas its own title rather than its file’s. Every public R6 method is documented, soroxygenise()runs without a warning (there were 391);tools/document.Rregeneratesman/with the pinned roxygen2 release and fails on any warning, as does thedocs-driftCI job, and the test suite rejects the roxygen layouts that produce a page describing the wrong object. - The README lists every roadmap-added layer type under “Experimental Plot Types”, the examples gallery has a worked example for each Base R and ggplot2 chart, and
use_cdnis documented as it behaves:show()andsave_html()use the bundled files by default (with alib/folder beside the saved file), while widgets, knitr and Shiny auto-detect the CDN. - The getting-started vignette tells Quarto
revealjsauthors how to keep off-slide charts out of the tab order. - The examples gallery on the package website is one article per plot family (bar and pie, distributions, scatter and line, heat map and candlestick, multi-panel and facet, and two Base R pages) linked from a short hub at the old URL, so each page stays well inside the 2 MB of HTML that Googlebot reads. The website also gains a
robots.txt, canonical links, per-page descriptions, and cross-links to the MAIDR JavaScript core and py-maidr. -
citation("maidr")returns the CHI 2024 and EuroVis 2024 MAIDR papers alongside the package entry (newinst/CITATION), and the README cites them. - The “Getting Help” sections of the getting-started and Shiny vignettes send bug reports to this package’s issue tracker rather than the JavaScript core’s, with link text that names the repository, and the README’s help section points at the function reference instead of back at the site it is on (#311).
-
save_html()is no longer described as writing a “standalone” or “portable” file. By default the MAIDR.js library goes into alib/folder beside the file and the two have to be shared together; an.htmlsent on its own loads no MAIDR.js and shows a plain chart. The description line, the README and the getting-started vignette now say so, and “standalone” is reserved foruse_cdn = TRUE(#319). - The README and the getting-started vignette carry one section, “How maidr hooks into your session”, saying what happens at the console (printing a ggplot2 object opens the viewer; Base R calls are recorded until
show()), in R Markdown and Quarto (maidr_on()in a setup chunk, which installs the knitr hooks thatlibrary(maidr)alone does not), in Shiny, and how to turn it off. The examples hub no longer implies that interception is off untilmaidr_on()is called,?maidr_onsays when the call is needed, and the reference index filesmaidr_on(),maidr_off()and?"maidr-options"under “Turning interception on and off” rather than under R Markdown alone (#318). - The README, the getting-started vignette and the examples hub carry one keyboard table, identical on all three and matched to the MAIDR core’s controls reference: Up/Down, layer switching with Page Up/Page Down, label mode, high contrast and the shortcut help are listed, Space is “repeat the current sound”, and the Enter/Space and Escape rows the vignette had, which the core does not bind, are gone. Each copy sits between markers and
tests/testthat/test-docs-key-table.Rfails when the three drift apart (#312). - What attaching maidr masks is documented for users.
?"base-r-wrappers", which every Base R autolink on the website already pointed at but which was hidden from the reference index and the search engines, is now a user-facing page listing the graphics, stats, base and methods functions maidr replaces, saying that each passes through to the original, thatshow()hands a non-plot object tomethods::show()and that scripts and packages should callmaidr::show()by name, and giving the attach order for vioplot, wordcloud and quantmod in one place. It is indexed under “What attaching maidr masks”, and the README and the getting-started vignette summarise it in their session section (#320). - Stable and experimental plot types can no longer be mistaken for each other. Outside the README’s definition tables, every experimental type named in a heading, list or table row of the example articles, the getting-started vignette,
?maidr,?maidr_gantt,?maidr_rocand the example scripts carries an [experimental] mark after its name, and an unmarked type is stable, the convention the MAIDR JavaScript core and py-maidr follow in their own docs. The README says so, and its stable table notes that ggplot2 contour and Base Rvioplot::vioplot()are experimental; the getting-started vignette gains an “Experimental Plot Types” section and lists Base R contour plots, and?maidrlists contour and candlestick charts. - Stable and experimental plot types are no longer interleaved where the docs list them: the stable ones come first and the experimental ones follow in a section of their own. The examples hub splits its plot families into “Stable plot families” and “Experimental plot families”, the pkgdown Articles menu gives the two wholly experimental Base R pages a section of their own, and the getting-started vignette’s stable list no longer names ggplot2
geom_contour()or Base Rvioplot::vioplot(), which its experimental list already covers. The README says so.
Performance
- Charts are exported to SVG with ‘svglite’ instead of ‘gridSVG’. gridSVG walked every grob in R; svglite draws with R’s graphics engine in C++. The exported document keeps the shape maidr’s selectors and maidr.js are written against – the same element ids, groups, Y-flip,
<use>points and presentation attributes – so highlighting, announcements and high-contrast mode are unchanged. Exporting is 3 to 7 times faster (a 10,000-point ggplot2 scatter in 0.4 s rather than 1.6 s, a 100 x 100 heat map in 0.5 s rather than 1.5 s, 2,000 segments in 2.2 s rather than 15 s), and the SVG is no larger, a little smaller for most charts.matplot()andsymbols()charts, which gridSVG could not export and which fell back to a static image, are now interactive. - Rendered SVGs no longer carry gridSVG’s inline
gridSVGCoordsandgridSVGMappingsscript blocks. Nothing in maidr.js or this package read them, and they made up about a quarter of every chart’s HTML, soshow(),save_html(), knitr and Shiny output is correspondingly smaller. - ggplot2 layer processors reuse one built plot and gtable per render, and the faceted and patchwork paths do the same per panel and leaf.
- Base R renders cache the replayed gtable instead of re-replaying every recorded call; candlestick SVG post-processing parses the document once; the bundled JS/CSS is read once per session.
- The chart data embedded in each SVG is serialised about ten times faster for layers with many points, with byte-identical output.
jsonlitespent 2 s of a 5 s render on the per-point records of a 10,000-point ggplot2 scatter; that render now takes 2.7 s, and a 5,000-point line 0.9 s rather than 1.9 s.
maidr 0.4.0
CRAN release: 2026-07-10
New Features
- Added candlestick (OHLC) chart support for ‘ggplot2’ via the ‘tidyquant’ package’s
geom_candlestick(). Each candle is exposed as a single navigable element withopen,high,low,close, optionalvolume, and computedtrend(Bull / Bear / Neutral) andvolatility(high − low) fields. - Added Base R candlestick (OHLC) chart support via
quantmod::chartSeries(x, type = "candlesticks"). The xts/zoo input is validated withquantmod::has.OHLC()and each row is emitted as a navigableCandlestickPointwithvalue(ISO date),open,high,low,close, computedtrend(Bull / Bear / Neutral) andvolatility(high − low) fields, plus optionalvolumewhenquantmod::has.Vo()isTRUE.
maidr 0.2.0
CRAN release: 2026-03-07
New Features
- Added violin plot support for ‘ggplot2’ (
geom_violin()), including both vertical and horizontal orientations. - Violin plots produce two interactive layers: a box-summary layer (
violin_box) with min, Q1, median, Q3, max highlights, and a KDE density-curve layer (violin_kde) with navigable density points. - Added Ramer-Douglas-Peucker (RDP) curve simplification to reduce KDE density points to ~30 per violin while preserving shape fidelity.
- SVG coordinate injection for violin KDE points enables accurate highlight positioning in the maidr frontend.
Enhancements
- Renamed option
maidr.enabledtomaidr.auto_showfor clarity. - Added
domMapping.iqrDirectionsupport for violin box layers, aligning with the existing box plot pattern for correct Q1/Q3 highlighting under gridSVG Y-flip transforms. - Added plot augmentation API (
augment_plot(),needs_augmentation()) to theLayerProcessorbase class, enabling processors to inject additional geom layers before rendering. - Added multi-layer expansion in the orchestrator for plot types that produce more than one maidr layer from a single geom.
Documentation
- Added violin plot examples to
show(),save_html(), vignettes, and example scripts. - Updated DESCRIPTION to list violin plots as a supported type.
maidr 0.1.1
Resubmission after CRAN archival. Fixes CRAN policy compliance issues.
Bug Fixes
- Removed all
assign(..., envir = .GlobalEnv)calls that violated CRAN policy. Base R function wrappers are now installed into the package namespace during.onLoadand controlled via an active/inactive flag, eliminating any modification of the user’s global environment. - Removed
attach()usage that produced R CMD check NOTE. - Fixed Rd documentation warning caused by unicode escape sequences in
prefix_to_currency_codeparameter documentation.
maidr 0.1.0
Initial CRAN release.
Features
-
show()- Display interactive, accessible visualizations from ggplot2 or Base R plots with keyboard navigation and screen reader support -
save_html()- Export accessible visualizations to standalone HTML files -
render_maidr()andmaidr_output()- Shiny integration for interactive web applications
Supported Plot Types
ggplot2 - Basic
- Bar charts (
geom_bar(),geom_col()) - Grouped/dodged bar charts (
position = "dodge") - Stacked bar charts (
position = "stack") - Histograms (
geom_histogram()) - Line plots (
geom_line()) - Scatter plots (
geom_point()) - Box plots (
geom_boxplot()) - Heatmaps (
geom_tile()) - Smooth/density curves (
geom_smooth(),geom_density())
ggplot2 - Advanced
- Faceted plots (
facet_wrap(),facet_grid()) - Multi-panel layouts with patchwork package
- Multi-layered plots (e.g., histogram + density, scatter + smooth)
Base R - Advanced
- Multi-panel plots (
par(mfrow),par(mfcol)) - Faceted-style plots (using
par()with loops) - Multi-layered plots (sequential plotting calls)