ChartsOrg / Charts

Beautiful charts for iOS/tvOS/OSX! The Apple side of the crossplatform MPAndroidChart.
Apache License 2.0
27.52k stars 5.99k forks source link

Feature Request:- Add Gradient to Bar chart Bars #4652

Open ShresthPratapSingh opened 3 years ago

ShresthPratapSingh commented 3 years ago

Is there a way to add gradient colours in bar chart ? if not can we expect a future version with this functionality ?

I also saw a pending PR regarding the same issue: PR #4411

protosse commented 3 years ago

i also need this feature, finally i found a solution.

enum GradientDirection {
    case leftToRight
    case rightToLeft
    case topToBottom
    case bottomToTop
}

class GradientBarChartRenderer: BarChartRenderer {
    var gradientColors: [NSUIColor] = []
    var gradientDirection: GradientDirection = .topToBottom

    typealias Buffer = [CGRect]
    fileprivate var _buffers = [Buffer]()

    override func initBuffers() {
        super.initBuffers()
        guard let barData = dataProvider?.barData else { return _buffers.removeAll() }

        if _buffers.count != barData.count {
            while _buffers.count < barData.count {
                _buffers.append(Buffer())
            }
            while _buffers.count > barData.count {
                _buffers.removeLast()
            }
        }

        _buffers = zip(_buffers, barData).map { buffer, set -> Buffer in
            let set = set as! BarChartDataSetProtocol
            let size = set.entryCount * (set.isStacked ? set.stackSize : 1)
            return buffer.count == size
                ? buffer
                : Buffer(repeating: .zero, count: size)
        }
    }

    private func prepareBuffer(dataSet: BarChartDataSetProtocol, index: Int) {
        guard
            let dataProvider = dataProvider,
            let barData = dataProvider.barData
        else { return }

        let barWidthHalf = CGFloat(barData.barWidth / 2.0)

        var bufferIndex = 0
        let containsStacks = dataSet.isStacked

        let isInverted = dataProvider.isInverted(axis: dataSet.axisDependency)
        let phaseY = CGFloat(animator.phaseY)

        for i in (0 ..< dataSet.entryCount).clamped(to: 0 ..< Int(ceil(Double(dataSet.entryCount) * animator.phaseX))) {
            guard let e = dataSet.entryForIndex(i) as? BarChartDataEntry else { continue }

            let x = CGFloat(e.x)
            let left = x - barWidthHalf
            let right = x + barWidthHalf

            var y = e.y

            if containsStacks, let vals = e.yValues {
                var posY = 0.0
                var negY = -e.negativeSum
                var yStart = 0.0

                // fill the stack
                for value in vals {
                    if value == 0.0 && (posY == 0.0 || negY == 0.0) {
                        // Take care of the situation of a 0.0 value, which overlaps a non-zero bar
                        y = value
                        yStart = y
                    } else if value >= 0.0 {
                        y = posY
                        yStart = posY + value
                        posY = yStart
                    } else {
                        y = negY
                        yStart = negY + abs(value)
                        negY += abs(value)
                    }

                    var top = isInverted
                        ? (y <= yStart ? CGFloat(y) : CGFloat(yStart))
                        : (y >= yStart ? CGFloat(y) : CGFloat(yStart))
                    var bottom = isInverted
                        ? (y >= yStart ? CGFloat(y) : CGFloat(yStart))
                        : (y <= yStart ? CGFloat(y) : CGFloat(yStart))

                    // multiply the height of the rect with the phase
                    top *= phaseY
                    bottom *= phaseY

                    let barRect = CGRect(x: left, y: top,
                                         width: right - left,
                                         height: bottom - top)
                    _buffers[index][bufferIndex] = barRect
                    bufferIndex += 1
                }
            } else {
                var top = isInverted
                    ? (y <= 0.0 ? CGFloat(y) : 0)
                    : (y >= 0.0 ? CGFloat(y) : 0)
                var bottom = isInverted
                    ? (y >= 0.0 ? CGFloat(y) : 0)
                    : (y <= 0.0 ? CGFloat(y) : 0)

                var topOffset: CGFloat = 0.0
                var bottomOffset: CGFloat = 0.0
                if let offsetView = dataProvider as? BarChartView {
                    let offsetAxis = offsetView.getAxis(dataSet.axisDependency)
                    if y >= 0 {
                        // situation 1
                        if offsetAxis.axisMaximum < y {
                            topOffset = CGFloat(y - offsetAxis.axisMaximum)
                        }
                        if offsetAxis.axisMinimum > 0 {
                            bottomOffset = CGFloat(offsetAxis.axisMinimum)
                        }
                    }
                    else // y < 0
                    {
                        // situation 2
                        if offsetAxis.axisMaximum < 0 {
                            topOffset = CGFloat(offsetAxis.axisMaximum * -1)
                        }
                        if offsetAxis.axisMinimum > y {
                            bottomOffset = CGFloat(offsetAxis.axisMinimum - y)
                        }
                    }
                    if isInverted {
                        // situation 3 and 4
                        // exchange topOffset/bottomOffset based on 1 and 2
                        // see diagram above
                        (topOffset, bottomOffset) = (bottomOffset, topOffset)
                    }
                }
                // apply offset
                top = isInverted ? top + topOffset : top - topOffset
                bottom = isInverted ? bottom - bottomOffset : bottom + bottomOffset

                // multiply the height of the rect with the phase
                // explicitly add 0 + topOffset to indicate this is changed after adding accessibility support (#3650, #3520)
                if top > 0 + topOffset {
                    top *= phaseY
                } else {
                    bottom *= phaseY
                }

                let barRect = CGRect(x: left, y: top,
                                     width: right - left,
                                     height: bottom - top)
                _buffers[index][bufferIndex] = barRect
                bufferIndex += 1
            }
        }
    }

    override func drawDataSet(context: CGContext, dataSet: BarChartDataSetProtocol, index: Int) {
        super.drawDataSet(context: context, dataSet: dataSet, index: index)

        guard let dataProvider = dataProvider else { return }
        let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency)

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

        let buffer = _buffers[index]
        for j in buffer.indices {
            let barRect = buffer[j]
            drawRadianColor(context: context, rect: barRect)
        }
    }

    func drawRadianColor(context: CGContext, rect: CGRect) {
        if !gradientColors.isEmpty {
            let view = NSUIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
            gradientBackground(view: view, colors: gradientColors, direction: gradientDirection)
            if let image = self.image(with: view)?.cgImage {
                context.draw(image, in: rect)
            }
        }
    }

    func gradientBackground(view: NSUIView, colors: [NSUIColor], direction: GradientDirection) {
        let gradient = CAGradientLayer()
        gradient.frame = view.bounds
        gradient.colors = colors

        switch direction {
        case .leftToRight:
            gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
            gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
        case .rightToLeft:
            gradient.startPoint = CGPoint(x: 1.0, y: 0.5)
            gradient.endPoint = CGPoint(x: 0.0, y: 0.5)
        case .topToBottom:
            gradient.startPoint = CGPoint(x: 0.5, y: 1.0)
            gradient.endPoint = CGPoint(x: 0.5, y: 0.0)
        default:
            break
        }
        view.layer.insertSublayer(gradient, at: 0)
    }

    func image(with view: NSUIView) -> NSUIImage? {
        UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0.0)
        defer { UIGraphicsEndImageContext() }
        if let context = UIGraphicsGetCurrentContext() {
            view.layer.render(in: context)
            let image = UIGraphicsGetImageFromCurrentImageContext()
            return image
        }
        return nil
    }
}

use it with chartView.renderer = GradientBarChartRenderer(dataProvider: chartView, animator: chartView.chartAnimator, viewPortHandler: chartView.viewPortHandler)

dbrisinda commented 1 year ago

Hi, I read elsewhere that this was closed, however, I cannot find how to access the bar chart gradient in the latest release 4.0.0 of Sept. 22, 2022. Has it been added to the latest master branch or no?

dbrisinda commented 1 year ago

How do I use this swift class? I created a GradientBarChartRenderer.swift file and copy/pasted the above code into it. Also prepended @objc to the name of the class since I'm using it in an Objective-C project. How do I access the bar gradient rendering if my code with the standard bar chart looks like this:

    BarChartDataSet * dataSet = [[BarChartDataSet alloc] initWithEntries:values];
    [dataSet setColor:UIColor.blueColor];    
    NSMutableArray * dataSets = [[NSMutableArray alloc] init];
    [dataSets addObject:dataSet];
    BarChartData * data = [[BarChartData alloc] initWithDataSets:dataSets];              
    self.barChartView.data = data;
dbrisinda commented 1 year ago

In addition to the above, added the following code, and the bars just render in solid black:

    GradientBarChartRenderer * gradientRenderer = [[GradientBarChartRenderer alloc] initWithDataProvider:self.chartView animator:self.chartView.chartAnimator viewPortHandler:self.chartView.viewPortHandler];
    gradientRenderer.gradientColors = @[UIColor.redColor, UIColor.blueColor];
    self.chartView.renderer = gradientRenderer;
CosmicYogi commented 1 year ago
Screenshot 2023-06-14 at 6 43 27 PM

I tried but not not working for some reason @protosse

dipcse07 commented 6 months ago

Is the latest version of this library has gradientcolor opiton for bar Chart? cause i have not found it. but it is needed in any charts designs now a days. ....

dipcse07 commented 6 months ago

i have found the solution and it s working !! tested

`

import UIKit
import DGCharts

enum GradientDirection {
case leftToRight
case rightToLeft
case topToBottom
case bottomToTop
}

class GradientBarChartRenderer: BarChartRenderer  {       

var gradientColors: [NSUIColor] = []
var gradientDirection: GradientDirection = .topToBottom

typealias Buffer = [CGRect]
fileprivate var _buffers = [Buffer]()

override func initBuffers() {
    super.initBuffers()
    guard let barData = dataProvider?.barData else { return _buffers.removeAll() }

    if _buffers.count != barData.count {
        while _buffers.count < barData.count {
            _buffers.append(Buffer())
        }
        while _buffers.count > barData.count {
            _buffers.removeLast()
        }
    }

    _buffers = zip(_buffers, barData).map { buffer, set -> Buffer in
        let set = set as! BarChartDataSetProtocol
        let size = set.entryCount * (set.isStacked ? set.stackSize : 1)
        return buffer.count == size
            ? buffer
            : Buffer(repeating: .zero, count: size)
    }
}

private func prepareBuffer(dataSet: BarChartDataSetProtocol, index: Int) {
    guard
        let dataProvider = dataProvider,
        let barData = dataProvider.barData
    else { return }

    let barWidthHalf = CGFloat(barData.barWidth / 2.0)

    var bufferIndex = 0
    let containsStacks = dataSet.isStacked

    let isInverted = dataProvider.isInverted(axis: dataSet.axisDependency)
    let phaseY = CGFloat(animator.phaseY)

    for i in (0 ..< dataSet.entryCount).clamped(to: 0 ..< Int(ceil(Double(dataSet.entryCount) * animator.phaseX))) {
        guard let e = dataSet.entryForIndex(i) as? BarChartDataEntry else { continue }

        let x = CGFloat(e.x)
        let left = x - barWidthHalf
        let right = x + barWidthHalf

        var y = e.y

        if containsStacks, let vals = e.yValues {
            var posY = 0.0
            var negY = -e.negativeSum
            var yStart = 0.0

            // fill the stack
            for value in vals {
                if value == 0.0 && (posY == 0.0 || negY == 0.0) {
                    // Take care of the situation of a 0.0 value, which overlaps a non-zero bar
                    y = value
                    yStart = y
                } else if value >= 0.0 {
                    y = posY
                    yStart = posY + value
                    posY = yStart
                } else {
                    y = negY
                    yStart = negY + abs(value)
                    negY += abs(value)
                }

                var top = isInverted
                    ? (y <= yStart ? CGFloat(y) : CGFloat(yStart))
                    : (y >= yStart ? CGFloat(y) : CGFloat(yStart))
                var bottom = isInverted
                    ? (y >= yStart ? CGFloat(y) : CGFloat(yStart))
                    : (y <= yStart ? CGFloat(y) : CGFloat(yStart))

                // multiply the height of the rect with the phase
                top *= phaseY
                bottom *= phaseY

                let barRect = CGRect(x: left, y: top,
                                     width: right - left,
                                     height: bottom - top)
                _buffers[index][bufferIndex] = barRect
                bufferIndex += 1
            }
        } else {
            var top = isInverted
                ? (y <= 0.0 ? CGFloat(y) : 0)
                : (y >= 0.0 ? CGFloat(y) : 0)
            var bottom = isInverted
                ? (y >= 0.0 ? CGFloat(y) : 0)
                : (y <= 0.0 ? CGFloat(y) : 0)

            var topOffset: CGFloat = 0.0
            var bottomOffset: CGFloat = 0.0
            if let offsetView = dataProvider as? BarChartView {
                let offsetAxis = offsetView.getAxis(dataSet.axisDependency)
                if y >= 0 {
                    // situation 1
                    if offsetAxis.axisMaximum < y {
                        topOffset = CGFloat(y - offsetAxis.axisMaximum)
                    }
                    if offsetAxis.axisMinimum > 0 {
                        bottomOffset = CGFloat(offsetAxis.axisMinimum)
                    }
                }
                else // y < 0
                {
                    // situation 2
                    if offsetAxis.axisMaximum < 0 {
                        topOffset = CGFloat(offsetAxis.axisMaximum * -1)
                    }
                    if offsetAxis.axisMinimum > y {
                        bottomOffset = CGFloat(offsetAxis.axisMinimum - y)
                    }
                }
                if isInverted {
                    // situation 3 and 4
                    // exchange topOffset/bottomOffset based on 1 and 2
                    // see diagram above
                    (topOffset, bottomOffset) = (bottomOffset, topOffset)
                }
            }
            // apply offset
            top = isInverted ? top + topOffset : top - topOffset
            bottom = isInverted ? bottom - bottomOffset : bottom + bottomOffset

            // multiply the height of the rect with the phase
            // explicitly add 0 + topOffset to indicate this is changed after adding accessibility support (#3650, #3520)
            if top > 0 + topOffset {
                top *= phaseY
            } else {
                bottom *= phaseY
            }

            let barRect = CGRect(x: left, y: top,
                                 width: right - left,
                                 height: bottom - top)
            _buffers[index][bufferIndex] = barRect
            bufferIndex += 1
        }
    }
}

override func drawDataSet(context: CGContext, dataSet: BarChartDataSetProtocol, index: Int) {
    super.drawDataSet(context: context, dataSet: dataSet, index: index)

    guard let dataProvider = dataProvider else { return }
    let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency)

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

    let buffer = _buffers[index]
    for j in buffer.indices {
        let barRect = buffer[j]
        drawRadianColor(context: context, rect: barRect)
    }
}

func drawRadianColor(context: CGContext, rect: CGRect) {
    // Ensure the rect has non-zero dimensions
    guard rect.width > 0 && rect.height > 0 else {
        return
    }

    if !gradientColors.isEmpty {
        let gradientLayer = CAGradientLayer()
        gradientLayer.frame = CGRect(origin: .zero, size: rect.size) // Set frame to match the size of the rect

        let cgColors = gradientColors.map { $0.cgColor }
        gradientLayer.colors = cgColors

        switch gradientDirection {
        case .leftToRight:
            gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
            gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
        case .rightToLeft:
            gradientLayer.startPoint = CGPoint(x: 1.0, y: 0.5)
            gradientLayer.endPoint = CGPoint(x: 0.0, y: 0.5)
        case .topToBottom:
            gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
            gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
        case .bottomToTop:
            gradientLayer.startPoint = CGPoint(x: 0.5, y: 1.0)
            gradientLayer.endPoint = CGPoint(x: 0.5, y: 0.0)
        }

        // Create a UIImage from the gradientLayer
        UIGraphicsBeginImageContextWithOptions(gradientLayer.bounds.size, gradientLayer.isOpaque, 0.0)
        guard let gradientContext = UIGraphicsGetCurrentContext() else { return }
        gradientLayer.render(in: gradientContext)
        let gradientImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        // Draw the gradientImage onto the context
        if let gradientCGImage = gradientImage?.cgImage {
            context.draw(gradientCGImage, in: rect)
        }
    }
}

func image(with view: NSUIView) -> NSUIImage? {
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0.0)
    defer { UIGraphicsEndImageContext() }
    if let context = UIGraphicsGetCurrentContext() {
        view.layer.render(in: context)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        return image
    }
    return nil
  }
 }
  /*
  Use Case
      for barChartDataSet  
           barChartDataSet.colors = [.clear]

          let gradientRenderer = GradientBarChartRenderer( dataProvider: barChartView, animator: 
       barChartView.chartAnimator, viewPortHandler: barChartView.viewPortHandler)
      gradientRenderer.gradientColors = [isThisWeekTransactionDetails.value ? .transactionHistoryDetailsBlue : 
    .transactionDetailsRed, .white]
    barChartView.renderer = gradientRenderer

  */

`