{highcharter} and the accessibility module: Part 5

R data visualization accessibility highcharter Highcharts

The final installment of our series on using the Highcharts accessibility module with {highcharter}, in which we re-create an area chart with lots of annotations.

Mara Averick https://twitter.com/dataandme
2021-11-16

This final installment in our series on using the Highcharts accessibility module with {highcharter} is basically “extra credit.” (Read: I took the time to figure this out, so I’m sharing it. But, it’s probably not the most helpful post if you’re just trying to get the accessibility module working with {highcharter}.)

We covered enabling the accessibility module, and getting basic keyboard navigation in part one. In parts two and three we re-created two of the charts from Highcharts’ accessible demos. Lastly, in part four, we made a scatter plot inspired by Silvia Canelón’s compilation of accessible data visualization resources for R, and went into greater detail about the hidden screen-reader <div> that gets generated when you use the accessibility module.

All the annotations

Our {palmerpenguin} plot had a single annotation, which was recorded in the “Chart annotations summary” of the screen-reader section as an item in an unordered list.1

The chart we’re re-creating here (which plots elevation and distance of the 2017 Tour de France) has lots of annotations. We want these annotations to show up in the screen-reader section with the text itself and information about where it is on the chart (in terms and distance and elevation).

Up until now, we’ve been using elements of the Highcharts JavaScript API that are configured inside of Highcharts.chart({...});. For example, when we specify the valueSuffix for a tooltip using hc_tooltip(), we are referencing tooltip.valueSuffix, which is defined inside of Highcharts.chart({});.

However, there are some options that are defined outside of the chart object. These are set with Highcharts.setOptions({...});, and are divided up into global and lang. As it turns out, configuring accessibility strings takes place in lang.accessibility.

lang options

lang (AKA “the language object”) isn’t defined on a per-chart basis. As the Highcharts documentation describes:

The language object is global and it can’t be set on each chart initialization.

Taking a peek at the {highcharter} source code, it looks like lang options are set when the package is loaded. So, if I want to create a template for the description of the annotations (lang.accessibility.screenReaderSection.annotations), I’m going to have to do so before making my chart.

Tour de France 2017 chart

Once again, I made use of the Highcharts export module to get data out of the original chart in CSV format in order to read it back into R.

Show code
library(tidyverse)
url <- "https://gist.githubusercontent.com/batpigandme/1916d95323ddb29274bddf3316041fd3/raw/a8f394cb85b4ae995798c2596ffe912491d2fb7c/2017-tour-de-france-stage-8.csv"
tdf_data <- read_csv(url) %>%
  drop_na() # have some from data export

In the original the screen-reader text for annotations is set using the following (unevaluated) code:

    lang: {
        accessibility: {
            screenReaderSection: {
                annotations: {
                    descriptionNoPoints: '{annotationText}, at distance {annotation.options.point.x}km, elevation {annotation.options.point.y} meters.'
                }
            }
        }
    }

The “rules” (gleaned largely from the pattern-fills examples in the Modules & plugins vignette) are basically the same for the language object as they were for chart objects: each level of the JavaScript API becomes a nested list in your R code. The difference here is that we’re going to define them using options().

library(highcharter)

options(
  highcharter.lang = list(
    accessibility = list(
      screenReaderSection = list(
        annotations = list(
          descriptionNoPoints = '{annotationText}, at distance {annotation.options.point.x}km, elevation {annotation.options.point.y} meters.'
        )
      )
    )
  )
)

And now for the chart itself!

highchart() %>%
  hc_add_dependency(name = "modules/accessibility.js") %>%
  hc_add_dependency(name = "modules/annotations.js") %>%
  hc_add_dependency(name = "modules/exporting.js") %>%
  hc_add_dependency(name = "modules/export-data.js") %>%
  hc_add_series(tdf_data, "area", hcaes(x = Distance, y = Elevation),
                lineColor = "#434348",
                color = "#90ed7d",
                fillOpacity = 0.5,
                marker = list(enabled = FALSE)) %>%
  hc_xAxis(
    title = list(text = "Distance"),
    labels = list(format = "{value} km"),
    minRange = 5,
    accessibility = list(
      rangeDescription = "Range: 0 to 187.8 km."
    )
  ) %>%
  hc_yAxis(
    title = list(text = ""),
    labels = list(format = "{value} m"),
    startOnTick = TRUE,
    endOnTick = FALSE,
    maxPadding = 0.35,
    accessibility = list(
      description = "Elevation",
      rangeDescription = "Range: 0 to 1,553 meters"
    )
  ) %>%
  hc_title(text = "2017 Tour de France Stage 8: Dole - Station des Rousses") %>%
  hc_caption(text = "An annotated line chart illustrates the 8th stage of the 2017 Tour de France cycling race from the start point in Dole to the finish line at Station des Rousses. Altitude is plotted on the Y-axis, and distance is plotted on the X-axis. The line graph is interactive, and the user can trace the altitude level along the stage. The graph is shaded below the data line to visualize the mountainous altitudes encountered on the 187.5-kilometre stage. The three largest climbs are highlighted at Col de la Joux, Côte de Viry and the final 11.7-kilometer, 6.4% gradient climb to Montée de la Combe de Laisia Les Molunes which peaks at 1200 meters above sea level. The stage passes through the villages of Arbois, Montrond, Bonlieu, Chassal and Saint-Claude along the route.") %>%
  # begin annotations
  # note that the points are grouped into separate lists
  # so that they can be styled in those groups
  hc_annotations(
    list(
      labelOptions = list(
        backgroundColor = 'rgba(255,255,255,0.6)',
        verticalAlign = 'top',
        y = 15
      ),
      labels = list(
        list(
          point = list(xAxis = 0, yAxis = 0, x = 27.98, y = 255),
          text = "Arbois"
        ),
        list(
          point = list(xAxis = 0, yAxis = 0, x = 45.5, y = 611),
          text = "Montrond"
        ),
        list(
          point = list(xAxis = 0, yAxis = 0, x = 63, y = 651),
          text = "Mont-sur-Monnet"
        ),
        list(
          point = list(xAxis = 0, yAxis = 0, x = 84, y = 789),
          x = -10,
          text = "Bonlieu"
        ),
        list(
          point = list(xAxis = 0, yAxis = 0, x = 129.5, y = 382),
          text = "Chassal"
        ),
        list(
          point = list(xAxis = 0, yAxis = 0, x = 159, y = 443),
          text = "Saint-Claude"
        )
      )
    ),
    list(
      labels = list(
        list(
          point = list(xAxis = 0, yAxis = 0, x = 101.44, y = 1026),
          x = -30,
          text = "Col de la Joux"
        ),
        list(
          point = list(xAxis = 0, yAxis = 0, x = 138.5, y = 748),
          text = "Côte de Viry"
        ),
        list(
          point = list(xAxis = 0, yAxis = 0, x = 176.4, y = 1202),
          text = "Montée de la Combe <br>de Laisia Les Molunes"
        )
      )
    ),
    list(
      labelOptions = list(
        shape = "connector",
        align = "right",
        justify = FALSE,
        crop = TRUE,
        style = list(
          fontSize = "0.8em",
          textOutline = "1px white"
        )
      ),
      labels = list(
        list(
          point = list(xAxis = 0, yAxis = 0, x = 96.2, y = 783),
          text = "6.1 km climb <br>4.6% on avg."
        ),
        list(
          point = list(xAxis = 0, yAxis = 0, x = 134.5, y = 540),
          text = "7.6 km climb <br>5.2% on avg."
        ),
        list(
          point = list(xAxis = 0, yAxis = 0, x = 172.2, y = 925),
          text = "11.7 km climb <br>6.4% on avg."
        )
      )
    )
  ) %>%
  hc_tooltip(
    headerFormat = "Distance: {point.x:.1f} km<br>",
    pointFormat = "{point.y} m a. s. l.",
    shared = TRUE
  ) %>%
  hc_legend(enabled = FALSE) %>%
  hc_exporting(
    enabled = TRUE,
    accessibility = list(
      enabled = TRUE
    )
  ) %>%
  hc_plotOptions(
    accessibility = list(
      enabled = TRUE,
      keyboardNavigation = list(enabled = TRUE),
      linkedDescription = 'This line chart uses the Highcharts Annotations feature to place labels at various points of interest.
The labels are responsive and will be hidden to avoid overlap on small screens.
Image description: An annotated line chart illustrates the 8th stage of the 2017 Tour de France cycling race from the start point in Dole to the finish line at Station des Rousses.
Altitude is plotted on the Y-axis, and distance is plotted on the X-axis.
The line graph is interactive, and the user can trace the altitude level along the stage.
The graph is shaded below the data line to visualize the mountainous altitudes encountered on the 187.5-kilometre stage. The three largest climbs are highlighted at Col de la Joux, Côte de Viry and the final 11.7-kilometer, 6.4% gradient climb to Montée de la Combe de Laisia Les Molunes which peaks at 1200 meters above sea level.
The stage passes through the villages of Arbois, Montrond, Bonlieu, Chassal and Saint-Claude along the route.',
      landmarkVerbosity = "one"
    ),
    area = list(
      accessibility = list(
        description = "This line chart uses the Highcharts Annotations feature to place labels at various points of interest. The labels are responsive and will be hidden to avoid overlap on small screens. Image description: An annotated line chart illustrates the 8th stage of the 2017 Tour de France cycling race from the start point in Dole to the finish line at Station des Rousses. Altitude is plotted on the Y-axis, and distance is plotted on the X-axis. The line graph is interactive, and the user can trace the altitude level along the stage. The graph is shaded below the data line to visualize the mountainous altitudes encountered on the 187.5-kilometre stage. The three largest climbs are highlighted at Col de la Joux, Côte de Viry and the final 11.7-kilometer, 6.4% gradient climb to Montée de la Combe de Laisia Les Molunes which peaks at 1200 meters above sea level. The stage passes through the villages of Arbois, Montrond, Bonlieu, Chassal and Saint-Claude along the route."
      )
    )
  )

Did it work?

The proof is in the pudding, which, in our case, is the HTML. Behold! We get the following at the end of our screen-reader section (but with all of the annotations in place of ...):

<div>
"Chart annotations summary"
<ul style="list-style-type: none">
  <li>Arbois, at distance 27.98km, elevation 255 meters.</li>
  <li>Montrond, at distance 45.5km, elevation 611 meters.</li>
  ...
</ul>
</div>

Wrapping up

Though I haven’t figured out how to get all of the aspects of the Highcharts accessibility module working from {highcharter} (linkedDescription, I’m looking at you), I’m confident that the issue is with the user (yours truly) and is not some limitation of Joshua Kunst’s excellent R package.

There’s only so much that can (or should) be automatically generated when it comes to making a data visualization accessible, but Highcharts gives you some really nice scaffolding to work with.

I’m not an accessibility expert by any stretch of the imagination. If you want to learn more about accessibility and data visualization the collection from the dataviza11y group, “Dataviz Accessibility Resources: A non-exhaustive and in-progress list of people and resources in Accessibility and Data Visualization”, is a great place to start. I also really enjoyed an (as of this writing) recent paper by Alan Lundgard and Arvind Satyanarayan, Accessible Visualization via Natural Language Descriptions: A Four-Level Model of Semantic Content (Lundgard and Satyanarayan (2022)). If you’re an R user (likely, since you’re reading this post), Silvia Canelón’s compilation is most definitely worthy of a visit (Canelón (2021)).

Thank you to Joshua and Silvia for writing the package (Kunst (2021)) and inspiring me to embark on this endeavor, respectively.

Corrections, improvements, and suggestions are more than welcome!

Canelón, Silvia. 2021. “Resources for Data Viz Accessibility.” https://silvia.rbind.io/blog/2021-curated-compilations/01-data-viz-a11y/.
Kunst, Joshua. 2021. Highcharter: A Wrapper for the ’Highcharts’ Library. https://jkunst.com/highcharter.
Lundgard, Alan, and Arvind Satyanarayan. 2022. Accessible Visualization via Natural Language Descriptions: A Four-Level Model of Semantic Content.” IEEE Transactions on Visualization & Computer Graphics (Proceedings of IEEE VIS). https://doi.org/10.1109/TVCG.2021.3114770.

  1. That’s a mouthful—summary point: if you didn’t read part 4, go skim the screen-reader-div section of it to make better sense of this post.↩︎

References

Reuse

Text and figures are licensed under Creative Commons Attribution CC BY 4.0. The figures that have been reused from other sources don't fall under this license and can be recognized by a note in their caption: "Figure from ...".

Citation

For attribution, please cite this work as

Averick (2021, Nov. 16). dataand.me: {highcharter} and the accessibility module: Part 5. Retrieved from https://dataand.me/posts/2021-11-16-highcharter-and-the-accessibility-module-part-5/

BibTeX citation

@misc{averick2021{highcharter},
  author = {Averick, Mara},
  title = {dataand.me: {highcharter} and the accessibility module: Part 5},
  url = {https://dataand.me/posts/2021-11-16-highcharter-and-the-accessibility-module-part-5/},
  year = {2021}
}