Skip to content

Commit

Permalink
Added accessibility for Bubble charts. (ChartsOrg#1060)
Browse files Browse the repository at this point in the history
Updated BubbleChartRenderer to mirror LineChartRenderer's nested
use of accessibilityOrderedElements to populate accessibleChartElements.
Minor updates to comments in LineChartRenderer.
  • Loading branch information
mathewa6 authored and Shineeth Hamza committed Oct 31, 2018
1 parent 5cb233c commit af809a6
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 5 deletions.
92 changes: 89 additions & 3 deletions Source/Charts/Renderers/BubbleChartRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import CoreGraphics

open class BubbleChartRenderer: BarLineScatterCandleBubbleRenderer
{
/// A nested array of elements ordered logically (i.e not in visual/drawing order) for use with VoiceOver.
private lazy var accessibilityOrderedElements: [[NSUIAccessibilityElement]] = accessibilityCreateEmptyOrderedElements()

@objc open weak var dataProvider: BubbleChartDataProvider?

@objc public init(dataProvider: BubbleChartDataProvider, animator: Animator, viewPortHandler: ViewPortHandler)
Expand All @@ -35,10 +38,32 @@ open class BubbleChartRenderer: BarLineScatterCandleBubbleRenderer
let bubbleData = dataProvider.bubbleData
else { return }

for set in bubbleData.dataSets as! [IBubbleChartDataSet] where set.isVisible
// 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? BubbleChartView {
let chartDescriptionText = chart.chartDescription?.text ?? ""
let dataSetDescriptions = bubbleData.dataSets.map { $0.label ?? "" }
let dataSetDescriptionText = dataSetDescriptions.joined(separator: ", ")
let dataSetCount = bubbleData.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)
}

for (i, set) in (bubbleData.dataSets as! [IBubbleChartDataSet]).enumerated() where set.isVisible
{
drawDataSet(context: context, dataSet: set)
drawDataSet(context: context, dataSet: set, dataSetIndex: i)
}

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

private func getShapeSize(
Expand All @@ -57,7 +82,7 @@ open class BubbleChartRenderer: BarLineScatterCandleBubbleRenderer
private var _pointBuffer = CGPoint()
private var _sizeBuffer = [CGPoint](repeating: CGPoint(), count: 2)

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

Expand Down Expand Up @@ -116,6 +141,20 @@ open class BubbleChartRenderer: BarLineScatterCandleBubbleRenderer

context.setFillColor(color.cgColor)
context.fillEllipse(in: rect)

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

accessibilityOrderedElements[dataSetIndex].append(element)
}
}
}

Expand Down Expand Up @@ -282,4 +321,51 @@ open class BubbleChartRenderer: BarLineScatterCandleBubbleRenderer
high.setDraw(x: _pointBuffer.x, y: _pointBuffer.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.
private func accessibilityCreateEmptyOrderedElements() -> [[NSUIAccessibilityElement]]
{
guard let chart = dataProvider as? BubbleChartView 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.
private func createAccessibleElement(withIndex idx: Int,
container: BubbleChartView,
dataSet: IBubbleChartDataSet,
dataSetIndex: Int,
modifier: (NSUIAccessibilityElement) -> ()) -> NSUIAccessibilityElement
{
let element = NSUIAccessibilityElement(accessibilityContainer: container)
let xAxis = container.xAxis

guard let e = dataSet.entryForIndex(idx) else { return element }
guard let dataProvider = dataProvider else { return element }

// NOTE: The formatter can cause issues when the x-axis labels are consecutive ints.
// i.e. due to the Double conversion, if there are more than one data set that are grouped,
// there is the possibility of some labels being rounded up. A floor() might fix this, but seems to be a brute force solution.
let label = xAxis.valueFormatter?.stringForValue(e.x, axis: xAxis) ?? "\(e.x)"

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

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

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

modifier(element)

return element
}
}
3 changes: 1 addition & 2 deletions Source/Charts/Renderers/LineChartRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ open class LineChartRenderer: LineRadarRenderer
continue
}

// ---------------
// Accessibility element geometry
let scaleFactor: CGFloat = 3
let accessibilityRect = CGRect(x: pt.x - (scaleFactor * circleRadius),
y: pt.y - (scaleFactor * circleRadius),
Expand All @@ -693,7 +693,6 @@ open class LineChartRenderer: LineRadarRenderer

accessibilityOrderedElements[i].append(element)
}
// ---------------

if !dataSet.isDrawCirclesEnabled
{
Expand Down

0 comments on commit af809a6

Please sign in to comment.