diff --git a/Charts.xcodeproj/project.pbxproj b/Charts.xcodeproj/project.pbxproj index dc4108ab83..1468b2b058 100644 --- a/Charts.xcodeproj/project.pbxproj +++ b/Charts.xcodeproj/project.pbxproj @@ -1038,7 +1038,8 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = YES; + EAGER_LINKING = YES; + ENABLE_MODULE_VERIFIER = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; FRAMEWORK_VERSION = A; GCC_NO_COMMON_BLOCKS = YES; @@ -1055,12 +1056,14 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11"; MTL_ENABLE_DEBUG_INFO = YES; + OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-expression-type-checking=50"; PRODUCT_BUNDLE_IDENTIFIER = com.dcg.Charts; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_COMPILATION_MODE = singlefile; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 12.0; @@ -1231,6 +1234,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + EAGER_LINKING = YES; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; FRAMEWORK_VERSION = A; @@ -1248,6 +1252,7 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11"; MTL_ENABLE_DEBUG_INFO = NO; + OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-expression-type-checking=50"; PRODUCT_BUNDLE_IDENTIFIER = com.dcg.Charts; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; diff --git a/Source/Charts/Animation/ChartAnimationEasing.swift b/Source/Charts/Animation/ChartAnimationEasing.swift index a6453d9e0c..a289338603 100644 --- a/Source/Charts/Animation/ChartAnimationEasing.swift +++ b/Source/Charts/Animation/ChartAnimationEasing.swift @@ -249,7 +249,12 @@ internal struct EasingFunctions } position = position - 1.0 - return Double( 0.5 * (-pow(2.0, -10.0 * position) + 2.0) ) + + // compute partial result so Xcode's type-checker doesn't take too long + let partialResult: Double = -pow(2.0, -10.0 * position) + 2.0 + + // took 120ms to type check before breaking out partial result, above + return Double( 0.5 * partialResult ) } internal static let EaseInCirc = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in @@ -265,12 +270,17 @@ internal struct EasingFunctions internal static let EaseInOutCirc = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in var position: TimeInterval = elapsed / (duration / 2.0) + + // calculate partial result so Swift compiler doesn't lose its mind + let sqrtPartialResult: Double = sqrt(1.0 - position * position) if position < 1.0 { - return Double( -0.5 * (sqrt(1.0 - position * position) - 1.0) ) + // was 800ms to type check with inlined sqrt calculation, from above + return Double( -0.5 * (sqrtPartialResult - 1.0) ) } position -= 2.0 - return Double( 0.5 * (sqrt(1.0 - position * position) + 1.0) ) + // was 1500ms to type check with inlined sqrt calculation, from above + return Double( 0.5 * (sqrtPartialResult + 1.0) ) } internal static let EaseInElastic = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in @@ -328,13 +338,23 @@ internal struct EasingFunctions return Double( -0.5 * (pow(2.0, 10.0 * position) * sin((position * duration - s) * (2.0 * Double.pi) / p)) ) } position -= 1.0 - return Double( pow(2.0, -10.0 * position) * sin((position * duration - s) * (2.0 * Double.pi) / p) * 0.5 + 1.0 ) + + // Break out partial result so the Swift compiler doesn't lose its mind + let sinPartialResult: Double = sin((position * duration - s) * (2.0 * Double.pi) / p) + + // Original expression here, with the expression above inlined, took 600ms to type check + return Double( pow(2.0, -10.0 * position) * sinPartialResult * 0.5 + 1.0 ) } internal static let EaseInBack = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in let s: TimeInterval = 1.70158 var position: TimeInterval = elapsed / duration - return Double( position * position * ((s + 1.0) * position - s) ) + + // Break out partial result so the Swift compiler doesn't lose its mind + let partialResult: Double = ((s + 1.0) * position - s) + + // Original expression here, with partialResult inlined, took 260ms to type check + return Double( position * position * partialResult ) } internal static let EaseOutBack = { (elapsed: TimeInterval, duration: TimeInterval) -> Double in diff --git a/Source/Charts/Charts/ChartViewBase.swift b/Source/Charts/Charts/ChartViewBase.swift index cfa7094cdf..13944f09c1 100644 --- a/Source/Charts/Charts/ChartViewBase.swift +++ b/Source/Charts/Charts/ChartViewBase.swift @@ -456,7 +456,7 @@ open class ChartViewBase: NSUIView, ChartDataProvider, AnimatorDelegate } // set the indices to highlight - highlighted = [h] + highlighted = [h] if callDelegate { diff --git a/Source/Charts/Data/Implementations/Standard/BarChartDataSet.swift b/Source/Charts/Data/Implementations/Standard/BarChartDataSet.swift index 999e94e443..069c3a7ed0 100644 --- a/Source/Charts/Data/Implementations/Standard/BarChartDataSet.swift +++ b/Source/Charts/Data/Implementations/Standard/BarChartDataSet.swift @@ -51,6 +51,34 @@ open class BarChartDataSet: BarLineScatterCandleBubbleChartDataSet, BarChartData /// the overall entry count, including counting each stack-value individually private var _entryCountStacks = 0 + /// the corner radius applied to each data set + public var cornerRadius: CGFloat = 0.0 + + /// array of corners to be rounded + open var roundedCorners: UIRectCorner = [] { + didSet { + var invertedCorners: UIRectCorner = [] + if roundedCorners.contains(.topLeft) { + invertedCorners.insert(.bottomLeft) + } + if roundedCorners.contains(.topRight) { + invertedCorners.insert(.bottomRight) + } + if roundedCorners.contains(.bottomLeft) { + invertedCorners.insert(.topLeft) + } + if roundedCorners.contains(.bottomRight) { + invertedCorners.insert(.topRight) + } + if roundedCorners.contains(.allCorners) { + invertedCorners.insert(.allCorners) + } + roundedCornersInverted = invertedCorners + } + } + + open private(set) var roundedCornersInverted: UIRectCorner = [] + /// Calculates the total number of entries this DataSet represents, including /// stacks. All values belonging to a stack are calculated separately. private func calcEntryCountIncludingStacks(entries: [BarChartDataEntry]) diff --git a/Source/Charts/Data/Implementations/Standard/ChartData.swift b/Source/Charts/Data/Implementations/Standard/ChartData.swift index 81fc37b3a8..8481f25657 100644 --- a/Source/Charts/Data/Implementations/Standard/ChartData.swift +++ b/Source/Charts/Data/Implementations/Standard/ChartData.swift @@ -452,6 +452,10 @@ extension ChartData: MutableCollection get { return dataSets[position] } set { self._dataSets[position] = newValue } } + + public subscript(safe index: Index) -> Element? { + return indices.contains(index) ? self[index] : nil + } } // MARK: RandomAccessCollection diff --git a/Source/Charts/Data/Implementations/Standard/RadarChartData.swift b/Source/Charts/Data/Implementations/Standard/RadarChartData.swift index 2f8a2db7ae..6313408ae2 100644 --- a/Source/Charts/Data/Implementations/Standard/RadarChartData.swift +++ b/Source/Charts/Data/Implementations/Standard/RadarChartData.swift @@ -46,6 +46,6 @@ open class RadarChartData: ChartData @objc open override func entry(for highlight: Highlight) -> ChartDataEntry? { - return self[highlight.dataSetIndex].entryForIndex(Int(highlight.x)) + return self[safe: highlight.dataSetIndex]?.entryForIndex(Int(highlight.x)) } } diff --git a/Source/Charts/Data/Interfaces/BarChartDataSetProtocol.swift b/Source/Charts/Data/Interfaces/BarChartDataSetProtocol.swift index 3ea0903cce..fcd9277f6a 100644 --- a/Source/Charts/Data/Interfaces/BarChartDataSetProtocol.swift +++ b/Source/Charts/Data/Interfaces/BarChartDataSetProtocol.swift @@ -12,6 +12,10 @@ import Foundation import CoreGraphics +#if canImport(UIKit) +import UIKit +#endif + @objc public protocol BarChartDataSetProtocol: BarLineScatterCandleBubbleChartDataSetProtocol { @@ -39,4 +43,13 @@ public protocol BarChartDataSetProtocol: BarLineScatterCandleBubbleChartDataSetP /// array of labels used to describe the different values of the stacked bars var stackLabels: [String] { get set } + + /// the corner radius applied to each data set + var cornerRadius: CGFloat { get set } + + /// array of corners to be rounded + var roundedCorners: UIRectCorner { get set } + + /// array of corners to be rounded + var roundedCornersInverted: UIRectCorner { get } } diff --git a/Source/Charts/Renderers/BarChartRenderer.swift b/Source/Charts/Renderers/BarChartRenderer.swift index 2aa69a828f..1c88b96e4f 100644 --- a/Source/Charts/Renderers/BarChartRenderer.swift +++ b/Source/Charts/Renderers/BarChartRenderer.swift @@ -351,7 +351,17 @@ open class BarChartRenderer: BarLineScatterCandleBubbleRenderer guard viewPortHandler.isInBoundsRight(barRect.origin.x) else { break } context.setFillColor(dataSet.barShadowColor.cgColor) - context.fill(barRect) + + var roundedCorners = dataSet.roundedCorners + if let i = buffer.firstIndex(of: barRect), + let entry = dataSet.entryForIndex(i), + entry.y < 0 { + roundedCorners = dataSet.roundedCornersInverted + } + let bezierPath = UIBezierPath(roundedRect: barRect, byRoundingCorners: roundedCorners, + cornerRadii: .init(width: dataSet.cornerRadius, height: dataSet.cornerRadius)) + context.addPath(bezierPath.cgPath) + context.drawPath(using: .fill) } } @@ -379,7 +389,15 @@ open class BarChartRenderer: BarLineScatterCandleBubbleRenderer context.setFillColor(dataSet.color(atIndex: j).cgColor) } - context.fill(barRect) + var roundedCorners = dataSet.roundedCorners + if let entry = dataSet.entryForIndex(j), + entry.y < 0 { + roundedCorners = dataSet.roundedCornersInverted + } + let bezierPath = UIBezierPath(roundedRect: barRect, byRoundingCorners: roundedCorners, + cornerRadii: .init(width: dataSet.cornerRadius, height: dataSet.cornerRadius)) + context.addPath(bezierPath.cgPath) + context.drawPath(using: .fill) if drawBorder { @@ -701,7 +719,7 @@ open class BarChartRenderer: BarLineScatterCandleBubbleRenderer for high in indices { guard - let set = barData[high.dataSetIndex] as? BarChartDataSetProtocol, + let set = barData[safe: high.dataSetIndex] as? BarChartDataSetProtocol, set.isHighlightEnabled else { continue } @@ -744,7 +762,14 @@ open class BarChartRenderer: BarLineScatterCandleBubbleRenderer setHighlightDrawPos(highlight: high, barRect: barRect) - context.fill(barRect) + var roundedCorners = set.roundedCorners + if e.y < 0 { + roundedCorners = set.roundedCornersInverted + } + let bezierPath = UIBezierPath(roundedRect: barRect, byRoundingCorners: roundedCorners, + cornerRadii: .init(width: set.cornerRadius, height: set.cornerRadius)) + context.addPath(bezierPath.cgPath) + context.drawPath(using: .fill) } } } diff --git a/Source/Charts/Renderers/BarLineScatterCandleBubbleRenderer.swift b/Source/Charts/Renderers/BarLineScatterCandleBubbleRenderer.swift index 2f4751d1aa..3478c30745 100644 --- a/Source/Charts/Renderers/BarLineScatterCandleBubbleRenderer.swift +++ b/Source/Charts/Renderers/BarLineScatterCandleBubbleRenderer.swift @@ -66,7 +66,9 @@ open class BarLineScatterCandleBubbleRenderer: NSObject, DataRenderer open func isDrawingValuesAllowed(dataProvider: ChartDataProvider?) -> Bool { guard let data = dataProvider?.data else { return false } - return data.entryCount < Int(CGFloat(dataProvider?.maxVisibleCount ?? 0) * viewPortHandler.scaleX) + let count = CGFloat(dataProvider?.maxVisibleCount ?? 0) * viewPortHandler.scaleX + guard count < CGFloat.infinity, !count.isNaN else { return false } + return data.entryCount < Int(count) } /// Class representing the bounds of the current viewport in terms of indices in the values array of a DataSet. diff --git a/Source/Charts/Renderers/BubbleChartRenderer.swift b/Source/Charts/Renderers/BubbleChartRenderer.swift index fa96a6f91e..60ec7d442a 100644 --- a/Source/Charts/Renderers/BubbleChartRenderer.swift +++ b/Source/Charts/Renderers/BubbleChartRenderer.swift @@ -252,7 +252,7 @@ open class BubbleChartRenderer: BarLineScatterCandleBubbleRenderer for high in indices { guard - let dataSet = bubbleData[high.dataSetIndex] as? BubbleChartDataSetProtocol, + let dataSet = bubbleData[safe: high.dataSetIndex] as? BubbleChartDataSetProtocol, dataSet.isHighlightEnabled, let entry = dataSet.entryForXValue(high.x, closestToY: high.y) as? BubbleChartDataEntry, isInBoundsX(entry: entry, dataSet: dataSet) diff --git a/Source/Charts/Renderers/CandleStickChartRenderer.swift b/Source/Charts/Renderers/CandleStickChartRenderer.swift index 40f80efb52..36b8c2b9e0 100644 --- a/Source/Charts/Renderers/CandleStickChartRenderer.swift +++ b/Source/Charts/Renderers/CandleStickChartRenderer.swift @@ -362,7 +362,7 @@ open class CandleStickChartRenderer: LineScatterCandleRadarRenderer for high in indices { guard - let set = candleData[high.dataSetIndex] as? CandleChartDataSetProtocol, + let set = candleData[safe: high.dataSetIndex] as? CandleChartDataSetProtocol, set.isHighlightEnabled else { continue } diff --git a/Source/Charts/Renderers/CombinedChartRenderer.swift b/Source/Charts/Renderers/CombinedChartRenderer.swift index de6121df63..77d114c427 100644 --- a/Source/Charts/Renderers/CombinedChartRenderer.swift +++ b/Source/Charts/Renderers/CombinedChartRenderer.swift @@ -162,7 +162,9 @@ open class CombinedChartRenderer: NSObject, DataRenderer open func isDrawingValuesAllowed(dataProvider: ChartDataProvider?) -> Bool { guard let data = dataProvider?.data else { return false } - return data.entryCount < Int(CGFloat(dataProvider?.maxVisibleCount ?? 0) * viewPortHandler.scaleX) + let count = CGFloat(dataProvider?.maxVisibleCount ?? 0) * viewPortHandler.scaleX + guard count < CGFloat.infinity, !count.isNaN else { return false } + return data.entryCount < Int(count) } /// All sub-renderers. diff --git a/Source/Charts/Renderers/HorizontalBarChartRenderer.swift b/Source/Charts/Renderers/HorizontalBarChartRenderer.swift index d2fed814f3..bbe267c918 100644 --- a/Source/Charts/Renderers/HorizontalBarChartRenderer.swift +++ b/Source/Charts/Renderers/HorizontalBarChartRenderer.swift @@ -228,7 +228,15 @@ open class HorizontalBarChartRenderer: BarChartRenderer _barShadowRectBuffer.size.width = viewPortHandler.contentWidth context.setFillColor(dataSet.barShadowColor.cgColor) - context.fill(_barShadowRectBuffer) + + var roundedCorners = dataSet.roundedCorners + if e.x < 0 { + roundedCorners = dataSet.roundedCornersInverted + } + let bezierPath = UIBezierPath(roundedRect: _barShadowRectBuffer, byRoundingCorners: roundedCorners, + cornerRadii: .init(width: dataSet.cornerRadius, height: dataSet.cornerRadius)) + context.addPath(bezierPath.cgPath) + context.drawPath(using: .fill) } } @@ -265,7 +273,15 @@ open class HorizontalBarChartRenderer: BarChartRenderer context.setFillColor(dataSet.color(atIndex: j).cgColor) } - context.fill(barRect) + var roundedCorners = dataSet.roundedCorners + if let entry = dataSet.entryForIndex(j), + entry.x < 0 { + roundedCorners = dataSet.roundedCornersInverted + } + let bezierPath = UIBezierPath(roundedRect: barRect, byRoundingCorners: roundedCorners, + cornerRadii: .init(width: dataSet.cornerRadius, height: dataSet.cornerRadius)) + context.addPath(bezierPath.cgPath) + context.drawPath(using: .fill) if drawBorder { @@ -614,9 +630,10 @@ open class HorizontalBarChartRenderer: BarChartRenderer open override func isDrawingValuesAllowed(dataProvider: ChartDataProvider?) -> Bool { - guard let data = dataProvider?.data - else { return false } - return data.entryCount < Int(CGFloat(dataProvider?.maxVisibleCount ?? 0) * self.viewPortHandler.scaleY) + guard let data = dataProvider?.data else { return false } + let count = CGFloat(dataProvider?.maxVisibleCount ?? 0) * viewPortHandler.scaleY + guard count < CGFloat.infinity, !count.isNaN else { return false } + return data.entryCount < Int(count) } /// Sets the drawing position of the highlight object based on the riven bar-rect. diff --git a/Source/Charts/Renderers/LineChartRenderer.swift b/Source/Charts/Renderers/LineChartRenderer.swift index ec622967ee..33cd05fad4 100644 --- a/Source/Charts/Renderers/LineChartRenderer.swift +++ b/Source/Charts/Renderers/LineChartRenderer.swift @@ -760,7 +760,7 @@ open class LineChartRenderer: LineRadarRenderer for high in indices { - guard let set = lineData[high.dataSetIndex] as? LineChartDataSetProtocol, + guard let set = lineData[safe: high.dataSetIndex] as? LineChartDataSetProtocol, set.isHighlightEnabled else { continue } diff --git a/Source/Charts/Renderers/PieChartRenderer.swift b/Source/Charts/Renderers/PieChartRenderer.swift index 18f571bc79..426c5be0c5 100644 --- a/Source/Charts/Renderers/PieChartRenderer.swift +++ b/Source/Charts/Renderers/PieChartRenderer.swift @@ -123,8 +123,6 @@ open class PieChartRenderer: NSObject, DataRenderer let drawAngles = chart.drawAngles let center = chart.centerCircleBox let radius = chart.radius - let drawInnerArc = chart.drawHoleEnabled && !chart.drawSlicesUnderHoleEnabled - let userInnerRadius = drawInnerArc ? radius * chart.holeRadiusPercent : 0.0 var visibleAngleCount = 0 for j in 0 ..< entryCount @@ -137,6 +135,8 @@ open class PieChartRenderer: NSObject, DataRenderer } let sliceSpace = visibleAngleCount <= 1 ? 0.0 : getSliceSpace(dataSet: dataSet) + let drawInnerArc = chart.drawHoleEnabled && !chart.drawSlicesUnderHoleEnabled || sliceSpace > 0.0 + let userInnerRadius = drawInnerArc ? radius * chart.holeRadiusPercent : 0.0 context.saveGState() @@ -180,7 +180,7 @@ open class PieChartRenderer: NSObject, DataRenderer continue } - let accountForSliceSpacing = sliceSpace > 0.0 && sliceAngle <= 180.0 + let accountForSliceSpacing = sliceSpace > 0.0 context.setFillColor(dataSet.color(atIndex: j).cgColor) @@ -207,6 +207,8 @@ open class PieChartRenderer: NSObject, DataRenderer if drawInnerArc && (innerRadius > 0.0 || accountForSliceSpacing) { + var hasZeroInnerRadius = innerRadius == 0.0 + if accountForSliceSpacing { var minSpacedRadius = calculateMinimumRadiusForSpacedSlice( @@ -235,42 +237,29 @@ open class PieChartRenderer: NSObject, DataRenderer } let endAngleInner = startAngleInner + sweepAngleInner - path.addLine( - to: CGPoint( - x: center.x + innerRadius * cos(endAngleInner.DEG2RAD), - y: center.y + innerRadius * sin(endAngleInner.DEG2RAD))) - - path.addRelativeArc(center: center, radius: innerRadius, startAngle: endAngleInner.DEG2RAD, delta: -sweepAngleInner.DEG2RAD) - } - else - { - if accountForSliceSpacing + if sliceAngle > 180.0 && hasZeroInnerRadius && accountForSliceSpacing { - let angleMiddle = startAngleOuter + sweepAngleOuter / 2.0 - - let sliceSpaceOffset = - calculateMinimumRadiusForSpacedSlice( - center: center, - radius: radius, - angle: sliceAngle * CGFloat(phaseY), - arcStartPointX: arcStartPointX, - arcStartPointY: arcStartPointY, - startAngle: startAngleOuter, - sweepAngle: sweepAngleOuter) - - let arcEndPointX = center.x + sliceSpaceOffset * cos(angleMiddle.DEG2RAD) - let arcEndPointY = center.y + sliceSpaceOffset * sin(angleMiddle.DEG2RAD) + let midAngleInner = (startAngleInner + endAngleInner) / 2 path.addLine( to: CGPoint( - x: arcEndPointX, - y: arcEndPointY)) + x: center.x + innerRadius * cos(midAngleInner.DEG2RAD), + y: center.y + innerRadius * sin(midAngleInner.DEG2RAD))) } else { - path.addLine(to: center) + path.addLine( + to: CGPoint( + x: center.x + innerRadius * cos(endAngleInner.DEG2RAD), + y: center.y + innerRadius * sin(endAngleInner.DEG2RAD))) + + path.addRelativeArc(center: center, radius: innerRadius, startAngle: endAngleInner.DEG2RAD, delta: -sweepAngleInner.DEG2RAD) } } + else + { + path.addLine(to: center) + } path.closeSubpath() @@ -518,7 +507,7 @@ open class PieChartRenderer: NSObject, DataRenderer { // calculate the text position let x = labelRadius * sliceXBase + center.x - let y = labelRadius * sliceYBase + center.y - lineHeight + let y = labelRadius * sliceYBase + center.y - lineHeight / 2.0 if drawXInside && drawYInside { @@ -589,7 +578,9 @@ open class PieChartRenderer: NSObject, DataRenderer open func isDrawingValuesAllowed(dataProvider: ChartDataProvider?) -> Bool { guard let data = dataProvider?.data else { return false } - return data.entryCount < Int(CGFloat(dataProvider?.maxVisibleCount ?? 0) * viewPortHandler.scaleX) + let count = CGFloat(dataProvider?.maxVisibleCount ?? 0) * viewPortHandler.scaleX + guard count < CGFloat.infinity, !count.isNaN else { return false } + return data.entryCount < Int(count) } /// draws the hole in the center of the chart and the transparent circle / hole @@ -716,8 +707,6 @@ open class PieChartRenderer: NSObject, DataRenderer let absoluteAngles = chart.absoluteAngles let center = chart.centerCircleBox let radius = chart.radius - let drawInnerArc = chart.drawHoleEnabled && !chart.drawSlicesUnderHoleEnabled - let userInnerRadius = drawInnerArc ? radius * chart.holeRadiusPercent : 0.0 // Append highlighted accessibility slices into this array, so we can prioritize them over unselected slices var highlightedAccessibleElements: [NSUIAccessibilityElement] = [] @@ -727,7 +716,7 @@ open class PieChartRenderer: NSObject, DataRenderer // get the index to highlight let index = Int(hightlight.x) guard index < drawAngles.count, - let set = data[hightlight.dataSetIndex] as? PieChartDataSetProtocol, + let set = data[safe: hightlight.dataSetIndex] as? PieChartDataSetProtocol, set.isHighlightEnabled else { @@ -755,6 +744,8 @@ open class PieChartRenderer: NSObject, DataRenderer } let sliceSpace = visibleAngleCount <= 1 ? 0.0 : set.sliceSpace + let drawInnerArc = chart.drawHoleEnabled && !chart.drawSlicesUnderHoleEnabled || sliceSpace > 0.0 + let userInnerRadius = drawInnerArc ? radius * chart.holeRadiusPercent : 0.0 let sliceAngle = drawAngles[index] var innerRadius = userInnerRadius @@ -762,7 +753,7 @@ open class PieChartRenderer: NSObject, DataRenderer let shift = set.selectionShift let highlightedRadius = radius + shift - let accountForSliceSpacing = sliceSpace > 0.0 && sliceAngle <= 180.0 + let accountForSliceSpacing = sliceSpace > 0.0 context.setFillColor(set.highlightColor?.cgColor ?? set.color(atIndex: index).cgColor) @@ -812,6 +803,8 @@ open class PieChartRenderer: NSObject, DataRenderer if drawInnerArc && (innerRadius > 0.0 || accountForSliceSpacing) { + var hasZeroInnerRadius = innerRadius == 0.0 + if accountForSliceSpacing { var minSpacedRadius = sliceSpaceRadius @@ -833,34 +826,31 @@ open class PieChartRenderer: NSObject, DataRenderer } let endAngleInner = startAngleInner + sweepAngleInner - path.addLine( - to: CGPoint( - x: center.x + innerRadius * cos(endAngleInner.DEG2RAD), - y: center.y + innerRadius * sin(endAngleInner.DEG2RAD))) - - path.addRelativeArc(center: center, radius: innerRadius, - startAngle: endAngleInner.DEG2RAD, - delta: -sweepAngleInner.DEG2RAD) - } - else - { - if accountForSliceSpacing + if sliceAngle > 180.0 && hasZeroInnerRadius && accountForSliceSpacing { - let angleMiddle = startAngleOuter + sweepAngleOuter / 2.0 - - let arcEndPointX = center.x + sliceSpaceRadius * cos(angleMiddle.DEG2RAD) - let arcEndPointY = center.y + sliceSpaceRadius * sin(angleMiddle.DEG2RAD) + let midAngleInner = (startAngleInner + endAngleInner) / 2 path.addLine( to: CGPoint( - x: arcEndPointX, - y: arcEndPointY)) + x: center.x + innerRadius * cos(midAngleInner.DEG2RAD), + y: center.y + innerRadius * sin(midAngleInner.DEG2RAD))) } else { - path.addLine(to: center) + path.addLine( + to: CGPoint( + x: center.x + innerRadius * cos(endAngleInner.DEG2RAD), + y: center.y + innerRadius * sin(endAngleInner.DEG2RAD))) + + path.addRelativeArc(center: center, radius: innerRadius, + startAngle: endAngleInner.DEG2RAD, + delta: -sweepAngleInner.DEG2RAD) } } + else + { + path.addLine(to: center) + } path.closeSubpath() diff --git a/Source/Charts/Renderers/RadarChartRenderer.swift b/Source/Charts/Renderers/RadarChartRenderer.swift index 21f1246049..bbba4bee0d 100644 --- a/Source/Charts/Renderers/RadarChartRenderer.swift +++ b/Source/Charts/Renderers/RadarChartRenderer.swift @@ -354,7 +354,7 @@ open class RadarChartRenderer: LineRadarRenderer for high in indices { guard - let set = chart.data?[high.dataSetIndex] as? RadarChartDataSetProtocol, + let set = chart.data?[safe: high.dataSetIndex] as? RadarChartDataSetProtocol, set.isHighlightEnabled else { continue } diff --git a/Source/Charts/Renderers/ScatterChartRenderer.swift b/Source/Charts/Renderers/ScatterChartRenderer.swift index 9fb1f560c7..d27a7a81ba 100644 --- a/Source/Charts/Renderers/ScatterChartRenderer.swift +++ b/Source/Charts/Renderers/ScatterChartRenderer.swift @@ -203,7 +203,7 @@ open class ScatterChartRenderer: LineScatterCandleRadarRenderer for high in indices { guard - let set = scatterData[high.dataSetIndex] as? ScatterChartDataSetProtocol, + let set = scatterData[safe: high.dataSetIndex] as? ScatterChartDataSetProtocol, set.isHighlightEnabled else { continue } diff --git a/Source/Charts/Utils/ChartUtils.swift b/Source/Charts/Utils/ChartUtils.swift index 3103fac6c3..b807fc1691 100644 --- a/Source/Charts/Utils/ChartUtils.swift +++ b/Source/Charts/Utils/ChartUtils.swift @@ -12,56 +12,40 @@ import Foundation import CoreGraphics -extension Comparable -{ - func clamped(to range: ClosedRange) -> Self - { - if self > range.upperBound - { +extension Comparable { + func clamped(to range: ClosedRange) -> Self { + if self > range.upperBound { return range.upperBound - } - else if self < range.lowerBound - { + } else if self < range.lowerBound { return range.lowerBound - } - else - { + } else { return self } } } -extension FloatingPoint -{ - var DEG2RAD: Self - { +extension FloatingPoint { + var DEG2RAD: Self { return self * .pi / 180 } - var RAD2DEG: Self - { + var RAD2DEG: Self { return self * 180 / .pi } - /// - Note: Value must be in degrees - /// - Returns: An angle between 0.0 < 360.0 (not less than zero, less than 360) - var normalizedAngle: Self - { + var normalizedAngle: Self { let angle = truncatingRemainder(dividingBy: 360) return (sign == .minus) ? angle + 360 : angle } } -extension CGSize -{ - func rotatedBy(degrees: CGFloat) -> CGSize - { +extension CGSize { + func rotatedBy(degrees: CGFloat) -> CGSize { let radians = degrees.DEG2RAD return rotatedBy(radians: radians) } - func rotatedBy(radians: CGFloat) -> CGSize - { + func rotatedBy(radians: CGFloat) -> CGSize { return CGSize( width: abs(width * cos(radians)) + abs(height * sin(radians)), height: abs(width * sin(radians)) + abs(height * cos(radians)) @@ -69,48 +53,34 @@ extension CGSize } } -extension Double -{ - /// Rounds the number to the nearest multiple of it's order of magnitude, rounding away from zero if halfway. - func roundedToNextSignificant() -> Double - { - guard - !isInfinite, - !isNaN, - self != 0 - else { return self } +extension Double { + func roundedToNextSignificant() -> Double { + guard !isInfinite, !isNaN, self != 0 else { return self } - let d = ceil(log10(self < 0 ? -self : self)) + // Use Foundation.log10 + let d = ceil(Foundation.log10(self < 0 ? -self : self)) let pw = 1 - Int(d) - let magnitude = pow(10.0, Double(pw)) + + // Use Foundation.pow + let magnitude = Foundation.pow(10.0, Double(pw)) let shifted = (self * magnitude).rounded() return shifted / magnitude } - var decimalPlaces: Int - { - guard - !isNaN, - !isInfinite, - self != 0.0 - else { return 0 } + var decimalPlaces: Int { + guard !isNaN, !isInfinite, self != 0.0 else { return 0 } let i = roundedToNextSignificant() - guard - !i.isInfinite, - !i.isNaN - else { return 0 } + guard !i.isInfinite, !i.isNaN else { return 0 } - return Int(ceil(-log10(i))) + 2 + // Use Foundation.log10 + return Int(ceil(-Foundation.log10(i))) + 2 } } -extension CGPoint -{ - /// Calculates the position around a center point, depending on the distance from the center, and the angle of the position around the center. - func moving(distance: CGFloat, atAngle angle: CGFloat) -> CGPoint - { +extension CGPoint { + func moving(distance: CGFloat, atAngle angle: CGFloat) -> CGPoint { return CGPoint(x: x + distance * cos(angle.DEG2RAD), y: y + distance * sin(angle.DEG2RAD)) } diff --git a/Source/Charts/Utils/Partition.swift b/Source/Charts/Utils/Partition.swift index 139f7e7349..803a1ac6cd 100644 --- a/Source/Charts/Utils/Partition.swift +++ b/Source/Charts/Utils/Partition.swift @@ -30,7 +30,7 @@ extension Collection { /// /// - Complexity: O(log *n*), where *n* is the length of this collection if /// the collection conforms to `RandomAccessCollection`, otherwise O(*n*). - func partitioningIndex( + public func partitioningIndex( where belongsInSecondPartition: (Element) throws -> Bool ) rethrows -> Index { var n = count diff --git a/Tests/ChartsTests/BarChartTests.swift b/Tests/ChartsTests/BarChartTests.swift index 2b0bdca9ce..acdf8383ec 100644 --- a/Tests/ChartsTests/BarChartTests.swift +++ b/Tests/ChartsTests/BarChartTests.swift @@ -367,4 +367,36 @@ class BarChartTests: XCTestCase { chart.notifyDataSetChanged() assertChartSnapshot(matching: chart) } + + func testBarShadow() { + let dataEntries = setupDefaultValuesDataEntries() + let dataSet = setupDefaultDataSet(chartDataEntries: dataEntries) + let chart = setupDefaultChart(dataSets: [dataSet]) + dataSet.barShadowColor = NSUIColor(red: 255.0/255.0, green: 187.0/255.0, blue: 115.0/255.0, alpha: 1.0) + chart.drawBarShadowEnabled = true + chart.notifyDataSetChanged() + assertChartSnapshot(matching: chart) + } + + func testHighlightSingleValue() { + let dataEntries = setupDefaultValuesDataEntries() + let dataSet = setupDefaultDataSet(chartDataEntries: dataEntries) + let chart = setupDefaultChart(dataSets: [dataSet]) + chart.notifyDataSetChanged() + chart.highlightValue(x: dataEntries[0].x, dataSetIndex: 0, callDelegate: false) + assertChartSnapshot(matching: chart) + } + + func testHighlightMultipleValue() { + let dataEntries = setupDefaultValuesDataEntries() + let dataSet = setupDefaultDataSet(chartDataEntries: dataEntries) + let chart = setupDefaultChart(dataSets: [dataSet]) + chart.notifyDataSetChanged() + chart.highlightValues( + [.init(x: dataEntries[0].x, y: dataEntries[0].y, dataSetIndex: 0), + .init(x: dataEntries[4].x, y: dataEntries[4].y, dataSetIndex: 0), + .init(x: dataEntries[5].x, y: dataEntries[5].y, dataSetIndex: 0)] + ) + assertChartSnapshot(matching: chart) + } } diff --git a/Tests/ChartsTests/__Snapshots__x86__/BarChartTests/testBarShadow-iOS_393-0_852-0.1.png b/Tests/ChartsTests/__Snapshots__x86__/BarChartTests/testBarShadow-iOS_393-0_852-0.1.png new file mode 100644 index 0000000000..9b0378edeb Binary files /dev/null and b/Tests/ChartsTests/__Snapshots__x86__/BarChartTests/testBarShadow-iOS_393-0_852-0.1.png differ diff --git a/Tests/ChartsTests/__Snapshots__x86__/BarChartTests/testHighlightMultipleValue-iOS_393-0_852-0.1.png b/Tests/ChartsTests/__Snapshots__x86__/BarChartTests/testHighlightMultipleValue-iOS_393-0_852-0.1.png new file mode 100644 index 0000000000..1f5cfc59e4 Binary files /dev/null and b/Tests/ChartsTests/__Snapshots__x86__/BarChartTests/testHighlightMultipleValue-iOS_393-0_852-0.1.png differ diff --git a/Tests/ChartsTests/__Snapshots__x86__/BarChartTests/testHighlightSingleValue-iOS_393-0_852-0.1.png b/Tests/ChartsTests/__Snapshots__x86__/BarChartTests/testHighlightSingleValue-iOS_393-0_852-0.1.png new file mode 100644 index 0000000000..1159b3cdc5 Binary files /dev/null and b/Tests/ChartsTests/__Snapshots__x86__/BarChartTests/testHighlightSingleValue-iOS_393-0_852-0.1.png differ