diff --git a/ChartsDemo-iOS/Swift/DemoBaseViewController.swift b/ChartsDemo-iOS/Swift/DemoBaseViewController.swift index 27e911a7a2..c4230ed8f3 100644 --- a/ChartsDemo-iOS/Swift/DemoBaseViewController.swift +++ b/ChartsDemo-iOS/Swift/DemoBaseViewController.swift @@ -21,8 +21,7 @@ enum Option { case toggleAutoScaleMinMax case toggleData case toggleBarBorders - // LineChart - case toggleGradientLine + case toggleGradient // CandleChart case toggleShadowColorSameAsCandle case toggleShowCandleBar @@ -63,7 +62,7 @@ enum Option { case .toggleData: return "Toggle Data" case .toggleBarBorders: return "Toggle Bar Borders" // LineChart - case .toggleGradientLine: return "Toggle Gradient Line" + case .toggleGradient: return "Toggle Gradient" // CandleChart case .toggleShadowColorSameAsCandle: return "Toggle shadow same color" case .toggleShowCandleBar: return "Toggle show candle bar" diff --git a/ChartsDemo-iOS/Swift/Demos/BarChartViewController.swift b/ChartsDemo-iOS/Swift/Demos/BarChartViewController.swift index 60be8856c1..1ab0a85b1e 100644 --- a/ChartsDemo-iOS/Swift/Demos/BarChartViewController.swift +++ b/ChartsDemo-iOS/Swift/Demos/BarChartViewController.swift @@ -25,6 +25,7 @@ class BarChartViewController: DemoBaseViewController { self.options = [.toggleValues, .toggleHighlight, + .toggleGradient, .animateX, .animateY, .animateXY, @@ -118,18 +119,20 @@ class BarChartViewController: DemoBaseViewController { } } - var set1: BarChartDataSet! = nil if let set = chartView.data?.first as? BarChartDataSet { - set1 = set - set1.values = yVals + set.values = yVals + + setup(set) + chartView.data?.notifyDataChanged() chartView.notifyDataSetChanged() } else { - set1 = BarChartDataSet(values: yVals, label: "The year 2017") - set1.colors = ChartColorTemplates.material() - set1.drawValuesEnabled = false - - let data = BarChartData(dataSet: set1) + let set = BarChartDataSet(values: yVals, label: "The year 2017") + set.drawValuesEnabled = false + + setup(set) + + let data = BarChartData(dataSet: set) data.setValueFont(UIFont(name: "HelveticaNeue-Light", size: 10)!) data.barWidth = 0.9 chartView.data = data @@ -137,9 +140,31 @@ class BarChartViewController: DemoBaseViewController { // chartView.setNeedsDisplay() } + + private func setup(_ dataSet: BarChartDataSet) { + if dataSet.drawBarGradientEnabled { + dataSet.colors = [.black, .red, .white] + dataSet.gradientPositions = [0, 40, 100] + } else { + dataSet.colors = ChartColorTemplates.material() + dataSet.gradientPositions = nil + } + } override func optionTapped(_ option: Option) { - super.handleOption(option, forChartView: chartView) + switch option { + case .toggleGradient: + + chartView.data?.dataSets + .compactMap { $0 as? BarChartDataSet } + .forEach { (set) in + set.drawBarGradientEnabled = !set.drawBarGradientEnabled + setup(set) + } + chartView.setNeedsDisplay() + default: + super.handleOption(option, forChartView: chartView) + } } // MARK: - Actions diff --git a/ChartsDemo-iOS/Swift/Demos/LineChart1ViewController.swift b/ChartsDemo-iOS/Swift/Demos/LineChart1ViewController.swift index 2d76df33ef..09cb4b89dc 100644 --- a/ChartsDemo-iOS/Swift/Demos/LineChart1ViewController.swift +++ b/ChartsDemo-iOS/Swift/Demos/LineChart1ViewController.swift @@ -30,7 +30,7 @@ class LineChart1ViewController: DemoBaseViewController { .toggleIcons, .toggleStepped, .toggleHighlight, - .toggleGradientLine, + .toggleGradient, .animateX, .animateY, .animateXY, @@ -194,7 +194,7 @@ class LineChart1ViewController: DemoBaseViewController { set.mode = (set.mode == .cubicBezier) ? .horizontalBezier : .cubicBezier } chartView.setNeedsDisplay() - case .toggleGradientLine: + case .toggleGradient: for set in chartView.data!.dataSets as! [LineChartDataSet] { set.isDrawLineWithGradientEnabled = !set.isDrawLineWithGradientEnabled setup(set) diff --git a/Source/Charts/Data/Implementations/Standard/BarChartDataSet.swift b/Source/Charts/Data/Implementations/Standard/BarChartDataSet.swift index cf86d883ee..ef5bf9c99e 100644 --- a/Source/Charts/Data/Implementations/Standard/BarChartDataSet.swift +++ b/Source/Charts/Data/Implementations/Standard/BarChartDataSet.swift @@ -104,7 +104,15 @@ open class BarChartDataSet: BarLineScatterCandleBubbleChartDataSet, BarChartData open var stackLabels: [String] = [] // MARK: - Styling functions and accessors - + + open var drawBarGradientEnabled = false + + open var gradientPositions: [CGFloat]? + + open var gradientStart: CGPoint = .infinite + + open var gradientEnd: CGPoint = .infinite + /// the color used for drawing the bar-shadows. The bar shadows is a surface behind the bar that indicates the maximum value open var barShadowColor = NSUIColor(red: 215.0/255.0, green: 215.0/255.0, blue: 215.0/255.0, alpha: 1.0) diff --git a/Source/Charts/Data/Interfaces/BarChartDataSetProtocol.swift b/Source/Charts/Data/Interfaces/BarChartDataSetProtocol.swift index 3e64d24f49..c5f372e616 100644 --- a/Source/Charts/Data/Interfaces/BarChartDataSetProtocol.swift +++ b/Source/Charts/Data/Interfaces/BarChartDataSetProtocol.swift @@ -39,4 +39,12 @@ public protocol BarChartDataSetProtocol: BarLineScatterCandleBubbleChartDataSetP /// array of labels used to describe the different values of the stacked bars var stackLabels: [String] { get set } + + var drawBarGradientEnabled: Bool { get set } + + var gradientPositions: [CGFloat]? { get set } + + var gradientStart: CGPoint { get set } + + var gradientEnd: CGPoint { get set } } diff --git a/Source/Charts/Renderers/BarChartRenderer.swift b/Source/Charts/Renderers/BarChartRenderer.swift index de94e87d42..b3692ccde9 100644 --- a/Source/Charts/Renderers/BarChartRenderer.swift +++ b/Source/Charts/Renderers/BarChartRenderer.swift @@ -242,14 +242,11 @@ open class BarChartRenderer: BarLineScatterCandleBubbleRenderer guard let dataProvider = dataProvider else { return } let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency) - + let valueToPixelMatrix = trans.valueToPixelMatrix + prepareBuffer(dataSet: dataSet, index: index) trans.rectValuesToPixel(&_buffers[index]) - let borderWidth = dataSet.barBorderWidth - let borderColor = dataSet.barBorderColor - let drawBorder = borderWidth > 0.0 - context.saveGState() defer { context.restoreGState() } @@ -299,9 +296,109 @@ open class BarChartRenderer: BarLineScatterCandleBubbleRenderer context.fill(barRect) } } - + + if dataSet.drawBarGradientEnabled { + drawGradientBars(context: context, dataSet: dataSet, buffer: buffer, matrix: valueToPixelMatrix) + } else { + drawDefaultBars(context: context, dataSet: dataSet, dateSetIndex: index, buffer: buffer) + } + } + + private func drawGradientBars( + context: CGContext, + dataSet: BarChartDataSetProtocol, + buffer: BarChartRenderer.Buffer, + matrix: CGAffineTransform) { + + guard let gradientPositions = dataSet.gradientPositions else + { + assertionFailure("Must set `gradientPositions if `dataSet.drawBarGradientEnabled` is true") + return + } + + guard let boundingBox = buffer.union() else { return } + guard !boundingBox.isNull, !boundingBox.isInfinite, !boundingBox.isEmpty else { + return + } + + let drawBorder = dataSet.barBorderWidth > 0 + + let gradientStart = dataSet.gradientEnd.isInfinite ? + CGPoint(x: boundingBox.minX, y: boundingBox.minY) : + dataSet.gradientEnd.applying(matrix) + + let gradientEnd = dataSet.gradientStart.isInfinite ? + CGPoint(x: boundingBox.minX, y: boundingBox.maxY) : + dataSet.gradientStart.applying(matrix) + + var gradientColorComponents: [CGFloat] = [] + var gradientLocations: [CGFloat] = [] + + for position in gradientPositions.reversed() + { + let location = CGPoint(x: boundingBox.minX, y: position) + .applying(matrix) + let normalizedLocation = + (location.y - boundingBox.minY) / (boundingBox.maxY - boundingBox.minY) + switch normalizedLocation { + case ..<0: + gradientLocations.append(0) + case 0..<1: + gradientLocations.append(normalizedLocation) + case 1...: + gradientLocations.append(1) + default: + assertionFailure() + } + } + + for color in dataSet.colors.reversed() + { + guard let (r, g, b, a) = color.nsuirgba else { + continue + } + gradientColorComponents += [r, g, b, a] + } + + let baseColorSpace = CGColorSpaceCreateDeviceRGB() + guard let gradient = CGGradient( + colorSpace: baseColorSpace, + colorComponents: &gradientColorComponents, + locations: &gradientLocations, + count: gradientLocations.count) else { + return + } + + for barRect in buffer + { + context.saveGState() + defer { context.restoreGState() } + + guard viewPortHandler.isInBoundsLeft(barRect.maxX) else { continue } + guard viewPortHandler.isInBoundsRight(barRect.minX) else { break } + + context.beginPath() + context.addRect(barRect) + context.clip() + context.drawLinearGradient(gradient, start: gradientStart, end: gradientEnd, options: []) + + if drawBorder + { + context.setStrokeColor(dataSet.barBorderColor.cgColor) + context.setLineWidth(dataSet.barBorderWidth) + context.stroke(barRect) + } + } + } + + private func drawDefaultBars( + context: CGContext, + dataSet: BarChartDataSetProtocol, + dateSetIndex: Int, + buffer: BarChartRenderer.Buffer) { + let drawBorder = dataSet.barBorderWidth > 0 let isSingleColor = dataSet.colors.count == 1 - + if isSingleColor { context.setFillColor(dataSet.color(atIndex: 0).cgColor) @@ -314,22 +411,22 @@ open class BarChartRenderer: BarLineScatterCandleBubbleRenderer for j in buffer.indices { let barRect = buffer[j] - - guard viewPortHandler.isInBoundsLeft(barRect.origin.x + barRect.size.width) else { continue } - guard viewPortHandler.isInBoundsRight(barRect.origin.x) else { break } + + guard viewPortHandler.isInBoundsLeft(barRect.maxX) else { continue } + guard viewPortHandler.isInBoundsRight(barRect.minX) else { break } if !isSingleColor { // Set the color for the currently drawn value. If the index is out of bounds, reuse colors. context.setFillColor(dataSet.color(atIndex: j).cgColor) } - + context.fill(barRect) - + if drawBorder { - context.setStrokeColor(borderColor.cgColor) - context.setLineWidth(borderWidth) + context.setStrokeColor(dataSet.barBorderColor.cgColor) + context.setLineWidth(dataSet.barBorderWidth) context.stroke(barRect) } @@ -339,7 +436,7 @@ open class BarChartRenderer: BarLineScatterCandleBubbleRenderer let element = createAccessibleElement(withIndex: j, container: chart, dataSet: dataSet, - dataSetIndex: index, + dataSetIndex: dateSetIndex, stackSize: stackSize) { (element) in element.accessibilityFrame = barRect diff --git a/Source/Charts/Utils/ChartUtils.swift b/Source/Charts/Utils/ChartUtils.swift index 85835032b3..191aa7ab7c 100644 --- a/Source/Charts/Utils/ChartUtils.swift +++ b/Source/Charts/Utils/ChartUtils.swift @@ -12,6 +12,13 @@ import Foundation import CoreGraphics +extension Array where Element == CGRect { + func union() -> Element? { + guard !isEmpty else { return nil } + return reduce(nil, { $0?.union($1) ?? $1 }) + } +} + extension Comparable { func clamped(to range: ClosedRange) -> Self { if self > range.upperBound { @@ -101,6 +108,14 @@ extension Double extension CGPoint { + static var infinite: CGPoint { + return CGPoint(x: CGFloat.infinity, y: CGFloat.infinity) + } + + var isInfinite: Bool { + return x.isInfinite || y.isInfinite + } + /// 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 {