# adapted from Highcharts advanced annotations demo
# <https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/demo/annotations/>

# read in data ------------------------------------------------------------
suppressPackageStartupMessages(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
## Rows: 1881 Columns: 2
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## dbl (2): Distance, Elevation
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
library(highcharter)
## Registered S3 method overwritten by 'quantmod':
##   method            from
##   as.zoo.data.frame zoo
## Highcharts (www.highcharts.com) is a Highsoft software product which is
## not free for commercial and Governmental use

Test to see if you can define lang options that are set on load for highcharter (see in repo here). The Highcharts documentation describes that:

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

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.'
        )
      )
    )
  )
)

It worked!!

It looks like this might also work for highcharter.chart options, which are also set on load in the highcharter package (see here). These are chart-level (as opposed to plot-level options), so they don’t seem to work with hc_plotOptions().

Nope! Including the chunk immediately below gives us a chart with nothing in it… Need to investigate further. (I’m just leaving it there so I can see what I tried—eval is set to FALSE, since you’ll end up with an empty chart if you actually run it).

options(
  highcharter.chart = list(
    accessibility = list(
      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"  
    )
  )
)
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 = "This 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.") %>%
  # 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."
      )
    )
  )
# notes for next time -----------------------------------------------------

# n.b. the options below are chart-level (as opposed to plotOptions)
# for some reason, when I pass them in, the chart doesn't render
# hc_opts = list(
#   chart = list(
#     type = "area",
#     zoomType = "x",
#     panning = TRUE,
#     panKey = "shift",
#     scrollablePlotArea = list(
#       minWidth = 600
#     )
#   )
# )

# lang options are global and can't be set on each chart,
# this is the only place to access the accessibility template for
# the screenreader section for the annotated points
# <https://api.highcharts.com/highcharts/lang.accessibility.screenReaderSection.annotations>
# descriptions that are set in the labelOptions cannot use variables
# <https://api.highcharts.com/highcharts/annotations.labelOptions.accessibility.description>

What’s the benefit?

At this point, you might be thinking: “Wow, that’s a lot of code—what’s the payoff?”

I am a privileged, fully-sighted individual who does not rely on a screen reader, so I can’t truly phenomenologically assess the utility of the Highcharts accessibility module. However, even without a screen reader, you can “see” how your Highcharts visualization will “look” in the generated HTML, which includes a special <div> with information that comes from a templated screenReaderSection. In the case of the chart above, the outer HTML is:

<div id="highcharts-screen-reader-region-before-0" aria-label="Chart screen reader information." role="region" aria-hidden="false" style="position: relative;"></div>

Despite the fact that Highcharts and the {highcharter} R package both have excellent documentation, I used the contents of this section as a trial-and-error guide when making this visualization, especially with respect to the screen-reader visibility of the annotations.

The template for the contents is actually a customizable parameter (screenReadersection.beforeChartFormat), but I used the default format:

<{headingTagName}>{chartTitle}</{headingTagName}><div>{typeDescription}</div><div>{chartSubtitle}</div><div>{chartLongdesc}</div><div>{playAsSoundButton}</div><div>{viewTableButton}</div><div>{xAxisDescription}</div><div>{yAxisDescription}</div><div>{annotationsTitle}{annotationsList}</div>

So, for example, this chart ends up with the title, description, type of chart, and axis descriptions (as well as options to view the chart as a table, the code for which I’m not including). It begins:

<p>2017 Tour de France Stage 8: Dole - Station des Rousses</p>
<div>Chart with 1879 data points.</div>
<div>This 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.</div>

The axis descriptions include ranges, which can be automatically calculated or described manually as a rangeDescription. Ours read:

<div>The chart has 1 X axis displaying Distance. Range: 0 to 187.8 km.</div>
<div>The chart has 1 Y axis displaying Elevation. Range: 0 to 1,553 meters</div>

The annotations all end up in an unordered list inside of a “Chart annotation summary” <div>. Here are the first couple items in the one for this chart:

<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>

I haven’t yet figured out how to make the linkedDescription work with the higcharter package (you can see that I try to include it in several places in my R code). So, as of right now, that is missing from the screen-reader section. Joshua Kunst’s {highcharter} package really does give you access to the whole JavaScript API, so I’m confident that I’ll crack the code in due time.

Accessibility best practices suggest that there’s only so much that can be automatically generated when it comes to making a data visualization accessible (and I fully agree, of course). However, it’s definitely worth making use of the accessibility tools Highcharts provides.

You can learn more about Highcharts’ research in this article: Alison Bert and Lisa Marie Hayes. 2018. “Making Charts Accessible for People with Visual Impairments: A Collaboration between Elsevier and Highcharts Sets a New Standard for Chart Accessibility.” https://www.elsevier.com/connect/making-charts-accessible-for-people-with-visual-impairments

There are tons of other resources out there, and I’m not an expert. My best suggestion is to check out 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.”