Skip to content

Commit

Permalink
Added accessibility for Bar charts. (ChartsOrg#1060)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mathewa6 authored and Shineeth Hamza committed Oct 31, 2018
1 parent c7b8384 commit 075c270
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 15 deletions.
143 changes: 133 additions & 10 deletions Source/Charts/Renderers/BarChartRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,28 @@ import CoreGraphics

open class BarChartRenderer: BarLineScatterCandleBubbleRenderer
{
/// A nested array of elements ordered logically (i.e not in visual/drawing order) for use with VoiceOver
///
/// Its use is apparent when there are multiple data sets, since we want to read bars in left to right order,
/// irrespective of dataset. However, drawing is done per dataset, so using this array and then flattening it prevents us from needing to
/// re-render for the sake of accessibility.
///
/// In practise, its structure is:
///
/// ````
/// [
/// [dataset1 element1, dataset2 element1],
/// [dataset1 element2, dataset2 element2],
/// [dataset1 element3, dataset2 element3]
/// ...
/// ]
/// ````
/// This is done to provide numerical inference across datasets to a screenreader user, in the same way that a sighted individual
/// uses a multi-dataset bar chart.
///
/// The ````internal```` specifier is to allow subclasses (HorizontalBar) to populate the same array
internal lazy var accessibilityOrderedElements: [[NSUIAccessibilityElement]] = accessibilityCreateEmptyOrderedElements()

private class Buffer
{
var rects = [CGRect]()
Expand Down Expand Up @@ -187,6 +209,25 @@ open class BarChartRenderer: BarLineScatterCandleBubbleRenderer
let barData = dataProvider.barData
else { return }

// If we redraw the data, remove and repopulate accessible elements to update label values and frames
accessibleChartElements.removeAll()
accessibilityOrderedElements = accessibilityCreateEmptyOrderedElements()

// Make the chart header the first element in the accessible elements array
if let chart = dataProvider as? BarChartView {
let chartDescriptionText = chart.chartDescription?.text ?? ""
let dataSetDescriptions = barData.dataSets.map { $0.label ?? "" }
let dataSetDescriptionText = dataSetDescriptions.joined(separator: ", ")
let dataSetCount = barData.dataSets.count
let
element = NSUIAccessibilityElement(accessibilityContainer: chart)
element.accessibilityLabel = chartDescriptionText + ". \(dataSetCount) dataset\(dataSetCount == 1 ? "" : "s"). \(dataSetDescriptionText)"
element.accessibilityFrame = chart.bounds
element.isHeader = true
accessibleChartElements.append(element)
}

// Populate logically ordered nested elements into accessibilityOrderedElements in drawDataSet()
for i in 0 ..< barData.dataSetCount
{
guard let set = barData.getDataSetByIndex(i) else { continue }
Expand All @@ -201,19 +242,23 @@ open class BarChartRenderer: BarLineScatterCandleBubbleRenderer
drawDataSet(context: context, dataSet: set as! IBarChartDataSet, index: i)
}
}

// Merge nested ordered arrays into the single accessibleChartElements.
accessibleChartElements.append(contentsOf: accessibilityOrderedElements.flatMap { $0 } )
accessibilityPostLayoutChangedNotification()
}

private var _barShadowRectBuffer: CGRect = CGRect()

@objc open func drawDataSet(context: CGContext, dataSet: IBarChartDataSet, index: Int)
{
guard let dataProvider = dataProvider else { return }

let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency)

prepareBuffer(dataSet: dataSet, index: index)
trans.rectValuesToPixel(&_buffers[index].rects)

let borderWidth = dataSet.barBorderWidth
let borderColor = dataSet.barBorderColor
let drawBorder = borderWidth > 0.0
Expand Down Expand Up @@ -257,7 +302,7 @@ open class BarChartRenderer: BarLineScatterCandleBubbleRenderer
context.fill(_barShadowRectBuffer)
}
}

let buffer = _buffers[index]

// draw the bar shadow before the values
Expand Down Expand Up @@ -288,11 +333,15 @@ open class BarChartRenderer: BarLineScatterCandleBubbleRenderer
{
context.setFillColor(dataSet.color(atIndex: 0).cgColor)
}


// In case the chart is stacked, we need to accomodate individual bars within accessibilityOrdereredElements
let isStacked = dataSet.isStacked
let stackSize = isStacked ? dataSet.stackSize : 1

for j in stride(from: 0, to: buffer.rects.count, by: 1)
{
let barRect = buffer.rects[j]

if (!viewPortHandler.isInBoundsLeft(barRect.origin.x + barRect.size.width))
{
continue
Expand All @@ -317,6 +366,21 @@ open class BarChartRenderer: BarLineScatterCandleBubbleRenderer
context.setLineWidth(borderWidth)
context.stroke(barRect)
}

// Create and append the corresponding accessibility element to accessibilityOrderedElements
if let chart = dataProvider as? BarChartView
{
let element = createAccessibleElement(withIndex: j,
container: chart,
dataSet: dataSet,
dataSetIndex: index,
stackSize: stackSize)
{ (element) in
element.accessibilityFrame = barRect
}

accessibilityOrderedElements[j/stackSize].append(element)
}
}

context.restoreGState()
Expand Down Expand Up @@ -682,10 +746,69 @@ open class BarChartRenderer: BarLineScatterCandleBubbleRenderer

context.restoreGState()
}
/// Sets the drawing position of the highlight object based on the riven bar-rect.

/// Sets the drawing position of the highlight object based on the given bar-rect.
internal func setHighlightDrawPos(highlight high: Highlight, barRect: CGRect)
{
high.setDraw(x: barRect.midX, y: barRect.origin.y)
}

/// Creates a nested array of empty subarrays each of which will be populated with NSUIAccessibilityElements.
/// This is marked internal to support HorizontalBarChartRenderer as well.
internal func accessibilityCreateEmptyOrderedElements() -> [[NSUIAccessibilityElement]]
{
guard let chart = dataProvider as? BarChartView else { return [] }

let maxEntryCount = chart.data?.maxEntryCountSet?.entryCount ?? 0

return Array(repeating: [NSUIAccessibilityElement](),
count: maxEntryCount)
}

/// Creates an NSUIAccessibleElement representing the smallest meaningful bar of the chart
/// i.e. in case of a stacked chart, this returns each stack, not the combined bar.
/// Note that it is marked internal to support subclass modification in the HorizontalBarChart.
internal func createAccessibleElement(withIndex idx: Int,
container: BarChartView,
dataSet: IBarChartDataSet,
dataSetIndex: Int,
stackSize: Int,
modifier: (NSUIAccessibilityElement) -> ()) -> NSUIAccessibilityElement
{
let element = NSUIAccessibilityElement(accessibilityContainer: container)
let xAxis = container.xAxis

guard let e = dataSet.entryForIndex(idx/stackSize) as? BarChartDataEntry else { return element }
guard let dataProvider = dataProvider else { return element }

let label = xAxis.valueFormatter?.stringForValue(e.x, axis: xAxis) ?? "\(e.x)"

var elementValueText = dataSet.valueFormatter?.stringForValue(
e.y,
entry: e,
dataSetIndex: dataSetIndex,
viewPortHandler: viewPortHandler) ?? "\(e.y)"

if dataSet.isStacked, let vals = e.yValues
{
let stackLabel = dataSet.stackLabels[idx % stackSize]

elementValueText = dataSet.valueFormatter?.stringForValue(
vals[idx % stackSize],
entry: e,
dataSetIndex: dataSetIndex,
viewPortHandler: viewPortHandler) ?? "\(e.y)"

elementValueText = stackLabel + " \(elementValueText)"
}

let dataSetCount = dataProvider.barData?.dataSetCount ?? -1
let doesContainMultipleDataSets = dataSetCount > 1

element.accessibilityLabel = "\(doesContainMultipleDataSets ? (dataSet.label ?? "") + ", " : "") \(label): \(elementValueText)"

modifier(element)

return element
}
}
7 changes: 6 additions & 1 deletion Source/Charts/Renderers/ChartDataRendererBase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ import CoreGraphics
@objc(ChartDataRendererBase)
open class DataRenderer: Renderer
{
/// An array of elements that are presented to the ChartViewBase accessibility methods.
/// An array of accessibility elements that are presented to the ChartViewBase accessibility methods.
///
/// Note that the order of elements in this array determines the order in which they are presented and navigated by
/// Accessibility clients such as VoiceOver.
///
/// Renderers should ensure that the order of elements makes sense to a client presenting an audio-only interface to a user.
/// Subclasses should populate this array in drawData() or drawDataSet() to make the chart accessible.
@objc final var accessibleChartElements: [NSUIAccessibilityElement] = []

Expand Down
2 changes: 1 addition & 1 deletion Source/Charts/Renderers/PieChartRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -879,7 +879,7 @@ open class PieChartRenderer: DataRenderer
context.restoreGState()
}

/// Creates a UIAccessibleElement representing a slice of the PieChart.
/// Creates an NSUIAccessibilityElement representing a slice of the PieChart.
/// The element only has it's container and label set based on the chart and dataSet. Use the modifier to alter traits and frame.
private func createAccessibleElement(withIndex idx: Int,
container: PieChartView,
Expand Down
4 changes: 1 addition & 3 deletions Source/Charts/Utils/Platform+Accessibility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,6 @@ open class NSUIAccessibilityElement: NSAccessibilityElement
}
}

// TODO: Make isSelected toggle a selected state in conjunction with a .valueChanged notification
/// A placeholder for parity with iOS. Has no effect.
final var isSelected: Bool = false
{
didSet
Expand Down Expand Up @@ -161,7 +159,7 @@ open class NSUIAccessibilityElement: NSAccessibilityElement
}
}

/// NOTE: setAccessibilityRole(.list) is called at init.
/// NOTE: setAccessibilityRole(.list) is called at init. See Platform.swift.
extension NSUIView: NSAccessibilityGroup
{
open override func accessibilityLabel() -> String?
Expand Down

0 comments on commit 075c270

Please sign in to comment.