Skip to content

Commit 8415be8

Browse files
authored
Add alt-text functionality (#4482)
1 parent 2902ec0 commit 8415be8

File tree

7 files changed

+219
-3
lines changed

7 files changed

+219
-3
lines changed

NAMESPACE

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ S3method(fortify,sfg)
4646
S3method(fortify,summary.glht)
4747
S3method(fortify,tbl)
4848
S3method(fortify,tbl_df)
49+
S3method(get_alt_text,ggplot)
50+
S3method(get_alt_text,ggplot_built)
51+
S3method(get_alt_text,gtable)
4952
S3method(ggplot,"function")
5053
S3method(ggplot,default)
5154
S3method(ggplot_add,"NULL")
@@ -390,6 +393,7 @@ export(geom_text)
390393
export(geom_tile)
391394
export(geom_violin)
392395
export(geom_vline)
396+
export(get_alt_text)
393397
export(get_element_tree)
394398
export(gg_dep)
395399
export(ggplot)

NEWS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ small selection of feature refinements.
44

55
## Features
66

7+
* Alt-text can now be added to a plot using the `alt` label, i.e
8+
`+ labs(alt = ...)`. Currently this alt text is not automatically propagated,
9+
but we plan to integrate into Shiny, RMarkdown, and other tools in the future.
10+
(@thomasp85, #4477)
11+
712
* Add support for the BrailleR package for creating descriptions of the plot
813
when rendered (@thomasp85, #4459)
914

R/labels.r

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ update_labels <- function(p, labels) {
4343
#' bottom-right of the plot by default.
4444
#' @param tag The text for the tag label which will be displayed at the
4545
#' top-left of the plot by default.
46+
#' @param alt,alt_insight Text used for the generation of alt-text for the plot.
47+
#' See [get_alt_text] for examples.
4648
#' @param ... A list of new name-value pairs. The name should be an aesthetic.
4749
#' @export
4850
#' @examples
@@ -67,9 +69,11 @@ update_labels <- function(p, labels) {
6769
#' p +
6870
#' labs(title = "title") +
6971
#' labs(title = NULL)
70-
labs <- function(..., title = waiver(), subtitle = waiver(), caption = waiver(), tag = waiver()) {
72+
labs <- function(..., title = waiver(), subtitle = waiver(), caption = waiver(),
73+
tag = waiver(), alt = waiver(), alt_insight = waiver()) {
7174
# .ignore_empty = "all" is needed to allow trailing commas, which is NOT a trailing comma for dots_list() as it's in ...
72-
args <- dots_list(..., title = title, subtitle = subtitle, caption = caption, tag = tag, .ignore_empty = "all")
75+
args <- dots_list(..., title = title, subtitle = subtitle, caption = caption,
76+
tag = tag, alt = alt, alt_insight = alt_insight, .ignore_empty = "all")
7377

7478
is_waive <- vapply(args, is.waive, logical(1))
7579
args <- args[!is_waive]
@@ -97,3 +101,147 @@ ylab <- function(label) {
97101
ggtitle <- function(label, subtitle = waiver()) {
98102
labs(title = label, subtitle = subtitle)
99103
}
104+
105+
#' Extract alt text from a plot
106+
#'
107+
#' This function returns a text that can be used as alt-text in webpages etc.
108+
#' Currently it will use the `alt` label, added with `+ labs(alt = <...>)`, or
109+
#' a return an empty string, but in the future it might try to generate an alt
110+
#' text from the information stored in the plot.
111+
#'
112+
#' @param p a ggplot object
113+
#' @param ... Currently ignored
114+
#'
115+
#' @return A text string
116+
#'
117+
#' @export
118+
#' @aliases alt_text
119+
#'
120+
#' @examples
121+
#' p <- ggplot(mpg, aes(displ, hwy)) +
122+
#' geom_point()
123+
#'
124+
#' # Returns an empty string
125+
#' get_alt_text(p)
126+
#'
127+
#' # A user provided alt text
128+
#' p <- p + labs(
129+
#' alt = paste("A scatterplot showing the negative correlation between engine",
130+
#' "displacement as a function of highway miles per gallon")
131+
#' )
132+
#'
133+
#' get_alt_text(p)
134+
#'
135+
get_alt_text <- function(p, ...) {
136+
UseMethod("get_alt_text")
137+
}
138+
#' @export
139+
get_alt_text.ggplot <- function(p, ...) {
140+
p$labels[["alt"]] %||% ""
141+
}
142+
#' @export
143+
get_alt_text.ggplot_built <- function(p, ...) {
144+
p$plot$labels[["alt"]] %||% ""
145+
}
146+
#' @export
147+
get_alt_text.gtable <- function(p, ...) {
148+
attr(p, "alt-label") %||% ""
149+
}
150+
151+
#' Generate an alt text from a plot
152+
#'
153+
#' This function returns a text that can be used as alt-text in webpages etc.
154+
#' It will synthesize one from the information in the plot itself, but you can
155+
#' add a conclusion to the synthesized text using `+ labs(alt_insight = <...>)`.
156+
#'
157+
#' There is no way an automatically generated description can compete with one
158+
#' written by a human with knowledge of what the plot shows and in which
159+
#' context. We urge users to write their own alt text if at all possible.
160+
#' Guidance to how an effective alt-text is written can be found in
161+
#' [Writing Alt Text for Data Visualization](https://medium.com/nightingale/writing-alt-text-for-data-visualization-2a218ef43f81)
162+
#' and [Effective Practices for Description of Science Content within Digital Talking Books](https://www.wgbh.org/foundation/ncam/guidelines/effective-practices-for-description-of-science-content-within-digital-talking-books)
163+
#'
164+
#' @param p a ggplot object
165+
#'
166+
#' @return A text string
167+
#'
168+
#' @noRd
169+
#'
170+
#' @examples
171+
#' p <- ggplot(mpg, aes(displ, hwy)) +
172+
#' geom_point()
173+
#'
174+
#' get_alt_text(p)
175+
#'
176+
#' p <- p + ggtitle("The relationship between displacement and yield in cars")
177+
#' get_alt_text(p)
178+
#'
179+
#' # It will use scale information if available
180+
#' p <- p + scale_x_continuous("highway miles per gallon")
181+
#' get_alt_text(p)
182+
#'
183+
#' # Add a short description of the main conclusion of the plot
184+
#' p <- p + labs(alt_insight = "The higher the yield, the lower the displacement")
185+
#' get_alt_text(p)
186+
#'
187+
#' # A user provided alt text takes precedence
188+
#' p <- p + labs(
189+
#' alt = paste("A scatterplot showing the negative correlation between engine",
190+
#' "displacement as a function of highway miles per gallon")
191+
#' )
192+
#'
193+
#' get_alt_text(p)
194+
#'
195+
generate_alt_text <- function(p) {
196+
# Combine titles
197+
title <- glue(glue_collapse(
198+
sub("\\.?$", "", c(p$labels$title, p$labels$subtitle)),
199+
last = ": "
200+
), ". ")
201+
title <- safe_string(title)
202+
203+
# Get axes descriptions
204+
axes <- glue(" showing ", glue_collapse(
205+
c(scale_description(p, "x"), scale_description(p, "y")),
206+
last = " and "
207+
))
208+
axes <- safe_string(axes)
209+
210+
# Get layer types
211+
layers <- vapply(p$layers, function(l) snake_class(l$geom), character(1))
212+
layers <- sub("_", " ", sub("^geom_", "", unique(layers)))
213+
layers <- glue(
214+
" using ",
215+
if (length(layers) == 1) "a " else "",
216+
glue_collapse(layers, sep = ", ", last = " and "),
217+
" layer",
218+
if (length(layers) == 1) "" else "s",
219+
)
220+
layers <- safe_string(layers)
221+
222+
# Combine
223+
alt <- glue_collapse(
224+
c(glue("{title}A plot{axes}{layers}"), p$labels$alt_insight),
225+
last = ". "
226+
)
227+
as.character(alt)
228+
}
229+
safe_string <- function(string) {
230+
if (length(string) == 0) "" else string
231+
}
232+
scale_description <- function(p, name) {
233+
scale <- p$scales$get_scales(name)
234+
if (is.null(scale)) {
235+
lab <- p$labels[[name]]
236+
type <- "the"
237+
} else {
238+
lab <- scale$make_title(scale$name %|W|% p$labels[[name]])
239+
type <- "a continuous"
240+
if (scale$is_discrete()) type <- "a discrete"
241+
if (inherits(scale, "ScaleBinned")) type <- "a binned"
242+
}
243+
if (is.null(lab)) {
244+
return(NULL)
245+
}
246+
glue("{lab} on {type} {name}-axis")
247+
}

R/plot-build.r

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ ggplot_build.ggplot <- function(plot) {
105105
# Let Layout modify data before rendering
106106
data <- layout$finish_data(data)
107107

108+
# Consolidate alt-text
109+
plot$labels$alt <- get_alt_text(plot)
110+
108111
structure(
109112
list(data = data, layout = layout, plot = plot),
110113
class = "ggplot_built"
@@ -402,6 +405,10 @@ ggplot_gtable.ggplot_built <- function(data) {
402405
plot_table$layout <- plot_table$layout[c(nrow(plot_table$layout), 1:(nrow(plot_table$layout) - 1)),]
403406
plot_table$grobs <- plot_table$grobs[c(nrow(plot_table$layout), 1:(nrow(plot_table$layout) - 1))]
404407
}
408+
409+
# add alt-text as attribute
410+
attr(plot_table, "alt-label") <- plot$labels$alt
411+
405412
plot_table
406413
}
407414

man/get_alt_text.Rd

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/labs.Rd

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-labels.r

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ test_that("Labels from default stat mapping are overwritten by default labels",
6262
expect_equal(p$labels$colour, "drv")
6363
})
6464

65+
test_that("alt text is returned", {
66+
p <- ggplot(mtcars, aes(mpg, disp)) +
67+
geom_point()
68+
expect_equal(get_alt_text(p), "")
69+
p <- p + labs(alt = "An alt text")
70+
expect_equal(get_alt_text(p), "An alt text")
71+
})
72+
6573

6674
# Visual tests ------------------------------------------------------------
6775

0 commit comments

Comments
 (0)