-
-
Notifications
You must be signed in to change notification settings - Fork 6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Accessibility Support for (most) Chart types #3520
Conversation
This commit makes PieChartView adhere to the UIAccessibility container protocol. See the Accessibility section marked for the methods. To allow conformance, 3 properties are added to IPieChartDataSet and PieChartDataSet that enable a more user friendly audio descriptionfor data elements. PieChartRenderer has a new private method createAccessibleElement() that populates the accessiblePieChartElements property, which in turn is used in PieChartView. Note that to prevent contextless audio descriptions, the default Chart Description header text was deleted in Description.swift since it is already an optional.
Minor changes to spacing in PieChartRenderer.swift. Removed formatting and use of "self." to match library style.
Updated ChartViewBase, ChartData and ChartDataRendererBase to declare the primary properties required for accessibility support within the Charts library. This includes the UIAccessibility protocol methods within ChartViewBase and the internal accessibleChartElements property in the base renderer. ChartData also has 3 optional properties to allow proper formatting of audio.
Added accessibilityChildren to ChartViewBase which is a layer over both UIAccessibilityContainer and NSAccessibilityGroup protocols. Updated PieChartRenderer to use the platform agnostic NSUIAccessibilityElement. Added init() overrides in NSUIView declaration for macOS to add .list NSAccessibilityRole. Added Platform+Accessibility.swift which extends NSUIView with accessibility container and group protocols and also declares NSUIAccessibilityElement, which acts as an abstraction over NSAccessibilityElement and UIAccessibilityElement.
Moved Platform+Accessibility.swift to Utils folder. Changed accessibleChartElements to be final since Renderer subclasses should not need to modify its working. Simply populating it in draw() functions will add basic accessibility.
Created internal property accessibilityOrderedElements to make BarChartRenderer be composed of logically ordered accessible elements (See inline comments for details). Updated ChartDataRendererBase, PieChartRenderer and Platform+Accessibility with updated comments to reflect the platform agnostic NSUIAccessibilityElement's use.
Updated HorizontalBarChartRenderer to populate accessibilityOrderedElements, which in turn is used by the BarChartRenderer superclass to enable accessibility. Added minor note to BarChartRenderer's createAccesibleElement() for edge case where x-axis labels can be inaccurate if multiple data sets are present.
LineChartRenderer now has a private accessibilityOrderedElements nested array that is then used to populate accessibleChartElements. Do note that the nesting is unnecessary for now, but will be needed once a custom rotor is added. Also unlike most other renderers, LineChartRenderer's accessibleChartElements is populated in drawCircles(). This required moving the check for isDrawCirclesEnabled() after accessibleChartElements are populated.
Updated BubbleChartRenderer to mirror LineChartRenderer's nested use of accessibilityOrderedElements to populate accessibleChartElements. Minor updates to comments in LineChartRenderer.
Updated RadarChartRenderer with accessibility properties and calls in drawData and drawDataSet. Due to the unique spatial arrangement of radar charts, accessibleChartElements is populated by dataset, within which variables are ordered in decreasing order.
Fixed crash due to incorrect use of maxEntryCountSet instead of dataSetCount in BubbleChartRenderer and LineChartRenderer's accessibilityCreateEmptyOrderedElements(). Updated BubbleChartRenderer's bubble accessibilityLabel to include percentage size of bubble based on maxSize property of IBubbleChartDataSet.
Updated CandleStickChartRenderer to populate accessibleChartElements. Unlike most other renderers with multiple dataSet support, we do not attempt to order elements logically and hence dataSets are presented to VO in the same order they are drawn with the dataSet label acting as a separating heading.
Added a workaround for non updating accessibility frame when resizing windows and using Charts on macOS by using setAccessibilityFrameInParent() in Platform+Accessibility. See inline comments for details.
Codecov Report
@@ Coverage Diff @@
## master #3520 +/- ##
==========================================
+ Coverage 29.26% 29.94% +0.67%
==========================================
Files 117 118 +1
Lines 13277 13793 +516
==========================================
+ Hits 3886 4130 +244
- Misses 9391 9663 +272
Continue to review full report at Codecov.
|
Do you have any idea why voice over is saying "padding" when going through the datasets? |
Great work btw. Seems to work good with the demos. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was there a reason for omitting the other chart type?
Note: We should also localize the voiceover strings. To be done in a future pull request.
let dataSetCount = barData.dataSets.count | ||
let | ||
element = NSUIAccessibilityElement(accessibilityContainer: chart) | ||
element.accessibilityLabel = chartDescriptionText + ". \(dataSetCount) dataset\(dataSetCount == 1 ? "" : "s"). \(dataSetDescriptionText)" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The screen reader reads "padding" at the end of the label.
let dataSetCount = bubbleData.dataSets.count | ||
let | ||
element = NSUIAccessibilityElement(accessibilityContainer: chart) | ||
element.accessibilityLabel = chartDescriptionText + ". \(dataSetCount) dataset\(dataSetCount == 1 ? "" : "s"). \(dataSetDescriptionText)" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The screen reader reads "padding" at the end of the label.
|
||
let | ||
element = NSUIAccessibilityElement(accessibilityContainer: chart) | ||
element.accessibilityLabel = "Candle Stick chart: " + description + ". \(entryCount) \(prefix + (entryCount == 1 ? "" : "s"))" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The screen reader reads "padding" at the end of the label.
let dataSetCount = lineData.dataSets.count | ||
let | ||
element = NSUIAccessibilityElement(accessibilityContainer: chart) | ||
element.accessibilityLabel = chartDescriptionText + ". \(dataSetCount) dataset\(dataSetCount == 1 ? "" : "s"). \(dataSetDescriptionText)" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The screen reader reads "padding" at the end of the label.
|
||
let | ||
element = NSUIAccessibilityElement(accessibilityContainer: chart) | ||
element.accessibilityLabel = description + ". \(entryCount) \(prefix + (entryCount == 1 ? "" : "s"))" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The screen reader reads "padding" at the end of the label.
let dataSetCount = radarData!.dataSets.count | ||
let | ||
element = NSUIAccessibilityElement(accessibilityContainer: chart) | ||
element.accessibilityLabel = chartDescriptionText + ". \(dataSetCount) dataset\(dataSetCount == 1 ? "" : "s")" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The screen reader reads "padding" at the end of the label.
self.accessibleChartElements.removeAll() | ||
|
||
// Make the chart header the first element in the accessible elements array | ||
if let chartDescriptionText: String = chart.chartDescription?.text { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this chart not say the datasets in the chart description is present?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this was because, for Radar chart, each individual dataSet is a heading element, so it's the next item in VO scroll order. Can definitely be added here, but I thought it was redundant, so let me know if you think I should add it.
// Otherwise, there is no non-visual logic to the data presented | ||
let accessibilityEntryValues = Array(0 ..< entryCount).map { (dataSet.entryForIndex($0)?.y ?? 0, $0) } | ||
let accessibilityAxisLabelValueTuples = zip(accessibilityXLabels, accessibilityEntryValues).map { ($0, $1.0, $1.1) }.sorted { $0.1 > $1.1 } | ||
let accessibilityDataSetDescription: String = description + ". \(entryCount) \(prefix + (entryCount == 1 ? "" : "s")). " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The screen reader reads "padding" at the end of the label.
@@ -18,6 +18,28 @@ import CoreGraphics | |||
|
|||
open class BarChartRenderer: BarLineScatterCandleBubbleRenderer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the demo project the "Another Bar Chart" demo has a frame that is too large around the bars.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see it! Will fix tomorrow.
Thanks! Regarding omission of Scatter & Combined charts: the same method using I will look into the "padding" issue: not sure why that occurs. |
Added createAccessibleHeader() to ChartRendererBase.swift that is used to create a descriptive header for all subclasses based on dataSet count and labels. RadarChartRenderer and PieChartRenderer updated to add dataSet label to each item and with documentation respectively.
Added an offset to BarChartRenderer's barRect calculation in prepareBuffer() to prevent calculation of rects outside visible chart area, while still allowing automatic offset of the axis minima visually. This workaround is only used when using auto calculated y-axis minima and isn't used when a custom minimum is manually set.
@petester42 Alright! I've addressed the rects in the AnotherBarChart sample and made the headers/titles more consistent overall. I cannot reproduce the "padding" issue. The word padding doesn't even appear in the project (excluding comments/variables) so I'm a little lost. |
Thanks for the changes. I still have the padding issue. I don't have any special settings, I just turned on voiceover without changing anything (iOS 11.4) and was trying out navigating by swiping through elements. If you can't find out why we can merge and then open an issue if other people have this problem. |
Is it possible that it is reading the CGRect for the frame? |
Interesting. I will play with it again and see if it could be the frame or perhaps a superclass' label. It doesn't occur on 11.4.1 beta or 12 beta. Will test on 11.4 as well to be sure. If you or another reviewer can try on another device and it still occurs, probably needs to be fixed before merge. |
It is a weird one for sure. I tried debugging it for a bit and my only thought would be that it is reading the whole element rather than just the label. I have an older phone with maybe an older version I’ll try it out there when I have time. |
Alright, in that case, it might be worth trying to see what happens if you remove any attributes like heading and set the label to some constant string literal. If it still shows then perhaps see if its there in other apps too? FYI it doesn't occur on 11.4 for me either. Thanks for looking into it and sorry about this! |
Alright. Thanks. For looking into this. I’ll check stuff out tonight and if it’s only my phone then I’ll merge. We can fix it later if it is an issue. |
Updated return value for the index(of:) function to be NSNotFound in Platform+Accessibility's iOS section. This is as required by the documentation for UIAccessibilityContainer protocol.
I couldn’t find out why. We can open an issue if people have problems. |
That's fine. I couldn't make it happen on another device either so I'm not too concerned. I'll be opening another PR for panning support with VO and the remaining chart types, so if it does become an issue I'd be willing to look again. Thanks for the merge! |
Issue Link 🔗
Accessibility Support (#1060) link
Goals ⚽
This PR adds VoiceOver (VO) support to the following chart types:
Implementation Details 🚧
This PR aimed to minimize intrusion with rendering or drawing of Charts as possible while still remaining usable. I'm leaving out support for Combined charts and Scatter charts for now for the same reason, as I think they need a bit more work to be made accessible in a usable manner (i.e. without an information overload to a VO user).
Accessibility is added as extensions to NSUIView, which is done in Platforms+Accessibility.swift to abstract differences between macOS/iOS. With that done, all individual ChartRenderers need to do is populate
accessibleChartElements
property of NSUIView during drawing. Some chart types like Bar Charts which can have multiple datasets/stacks use anaccessibilityOrderedElements
array internally to reorder elements to make more sense when using VO, rather than in drawing order. There are some minor optional properties in ChartData.swift to help make data sound better when presented over VO such as prefixes, suffixes to each data point.Testing Details 🔍
No changes to drawing, so none added. Manual on-device testing & audited for usability at WWDC.