Manohar
Manohar

Reputation: 41

Making Pie Chart with Slice space(Space between each color in Pie Chart)

Pie Chart Sample

I'm working on a project in which i need to implement the pie chart like above. I need to add a space between the slices as picture attached. Need to have a space in slices with arc and values with percentage.

I've tried to implement it but i couldn't succeed. I use get arc shapes wrong.

Please help me. Thanks.

import UIKit

private extension CGFloat {

/// Formats the CGFloat to a maximum of 1 decimal place.

    var formattedToOneDecimalPlace : String {
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        formatter.minimumFractionDigits = 0
        formatter.maximumFractionDigits = 1
        return formatter.string(from: NSNumber(value: self.native)) ?? "\(self)"
    }
}

/// Defines a segment of the pie chart

struct Segment {

    /// The color of the segment
    var color : UIColor

    /// The name of the segment
    var name : String

    /// The value of the segment
    var value : CGFloat
}

class PieChartView: UIView {

/// An array of structs representing the segments of the pie chart

    var segments = [Segment]() {
        didSet { setNeedsDisplay() } // re-draw view when the values get set
    }

    /// Defines whether the segment labels should be shown when drawing the pie chart
    var showSegmentLabels = true {
        didSet { setNeedsDisplay() }
    }

    /// Defines whether the segment labels will show the value of the segment in brackets
    var showSegmentValueInLabel = false {
        didSet { setNeedsDisplay() }
    }

    /// The font to be used on the segment labels
    var segmentLabelFont = UIFont.systemFont(ofSize: 14) {
        didSet {
            textAttributes[NSAttributedStringKey.font] = segmentLabelFont
            setNeedsDisplay()
        }
    }

    private let paragraphStyle : NSParagraphStyle = {
        var p = NSMutableParagraphStyle()
        p.alignment = .center
        return p.copy() as! NSParagraphStyle
    }()

    private lazy var textAttributes : [NSAttributedStringKey : Any] = {
        return [NSAttributedStringKey.paragraphStyle : self.paragraphStyle, NSAttributedStringKey.font : self.segmentLabelFont]
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        isOpaque = false // when overriding drawRect, you must specify this to maintain transparency.
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func draw(_ rect: CGRect) {

        // get current context
        let ctx = UIGraphicsGetCurrentContext()

        // radius is the half the frame's width or height (whichever is smallest)
        let radius = min(frame.width, frame.height) * 0.5

        // center of the view
        let viewCenter = CGPoint(x: bounds.size.width * 0.5, y: bounds.size.height * 0.5)

        // enumerate the total value of the segments by using reduce to sum them
        let valueCount = segments.reduce(0, {($0 + $1.value)})
        // the starting angle is -90 degrees (top of the circle, as the context is flipped). By default, 0 is the right hand side of the circle, with the positive angle being in an anti-clockwise direction (same as a unit circle in maths).
        var startAngle = -CGFloat.pi * 0.5

        // loop through the values array
        for segment in segments {

            // set fill color to the segment color
            ctx?.setFillColor(segment.color.cgColor)

            // update the end angle of the segment
            let endAngle = startAngle + .pi * 2 * (segment.value / valueCount)

            // move to the center of the pie chart
            ctx?.move(to: viewCenter)

            // add arc from the center for each segment (anticlockwise is specified for the arc, but as the view flips the context, it will produce a clockwise arc)
            ctx?.addArc(center: viewCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)

            // fill segment
            ctx?.fillPath()

           // ctx?.setStrokeColor(UIColor.white.cgColor)

           // ctx?.strokePath()


            if showSegmentLabels { // do text rendering

                // get the angle midpoint
                let halfAngle = startAngle + (endAngle - startAngle) * 0.5;

                // the ratio of how far away from the center of the pie chart the text will appear
                let textPositionValue : CGFloat = 0.65

                // get the 'center' of the segment. It's slightly biased to the outer edge, as it's wider.
                let segmentCenter = CGPoint(x: viewCenter.x + radius * textPositionValue * cos(halfAngle), y: viewCenter.y + radius * textPositionValue * sin(halfAngle))

                // text to render – the segment value is formatted to 1dp if needed to be displayed.

                // Previous
                //let textToRender = showSegmentValueInLabel ? "\(segment.name) \(segment.value.formattedToOneDecimalPlace))" : segment.name

                // Change
                let textToRender = showSegmentValueInLabel ? "\(segment.value.formattedToOneDecimalPlace)%" : segment.name

                // get the color components of the segement color
                guard let colorComponents = segment.color.cgColor.components else { return }

                // get the average brightness of the color
                let averageRGB = (colorComponents[0] + colorComponents[1] + colorComponents[2]) / 3

                // if too light, use black. If too dark, use white
                textAttributes[NSAttributedStringKey.foregroundColor] = (averageRGB > 0.7) ? UIColor.black : UIColor.white

                // the bounds that the text will occupy
                var renderRect = CGRect(origin: .zero, size: textToRender.size(withAttributes: textAttributes))

                // center the origin of the rect
                renderRect.origin = CGPoint(x: segmentCenter.x - renderRect.size.width * 0.5, y: segmentCenter.y - renderRect.size.height * 0.5)

                // draw text in the rect, with the given attributes
                textToRender.draw(in: renderRect, withAttributes: textAttributes)
            }

            // update starting angle of the next segment to the ending angle of this segment
            startAngle = endAngle
        }
    }
}

Upvotes: 1

Views: 1832

Answers (1)

Cosmos Man
Cosmos Man

Reputation: 603

In your draw(_:), implement this.

override func draw(_ rect: CGRect) {

    let anglePI2 = (CGFloat.pi * 2)
    let center = CGPoint.init(x: bounds.size.width / 2, y: bounds.size.height / 2)
    let radius = min(bounds.size.width, bounds.size.height) / 2;

    let lineWidth: CGFloat = 1;

    let ctx = UIGraphicsGetCurrentContext()
    ctx?.setLineWidth(lineWidth)


    var currentAngle: CGFloat = 0

    var totalValue: CGFloat = segments.reduce(0) { $0 + $1.value }
    if totalValue <= 0 {
        totalValue = 1
    }

    let iRange = 0 ..< segments.count
    for i in iRange {
        let segment = segments[i]
        // calculate percent
        let percent = segment.value / totalValue

        let angle = anglePI2 * percent

        ctx?.beginPath()
        ctx?.move(to: center)
        ctx?.addArc(center: center, radius: radius - lineWidth, startAngle: currentAngle, endAngle: currentAngle + angle, clockwise: false)
        ctx?.closePath()

        ctx?.setFillColor(segment.color.cgColor)
        ctx?.fillPath()

        ctx?.beginPath()
        ctx?.move(to: center)
        ctx?.addArc(center: center, radius: radius - (lineWidth / 2), startAngle: currentAngle, endAngle: currentAngle + angle, clockwise: false)
        ctx?.closePath()

        ctx?.setStrokeColor(UIColor.white.cgColor)
        ctx?.strokePath()

        currentAngle += angle
    }
}

Explanation
- Calculate pi equivalent to 360 deg.
- Calculate center.
- Calculate radius of the blocks.
- Calculate totalValue of [Segment]. This is used when calculating the percent of the slice in the circle.
- Loop through segment, calculate percentage of the current segment, make arc for filling (radius - lineWidth), remake arc for stroking path(radius - lineWidth / 2).

Here is the result enter image description here

In my view controller, I added the data (For your info). Like this

scv.segments = [
    Segment.init(color: UIColor.init(red: 0xfe/0xff, green: 0x4a/0xff, blue: 0x49/0xff, alpha: 1), value: 20),
    Segment.init(color: UIColor.init(red: 0x2a/0xff, green: 0xb7/0xff, blue: 0xca/0xff, alpha: 1), value: 15),
    Segment.init(color: UIColor.init(red: 0xfe/0xff, green: 0xd7/0xff, blue: 0x66/0xff, alpha: 1), value: 15),
    Segment.init(color: UIColor.init(red: 0x4a/0xff, green: 0x4e/0xff, blue: 0x4d/0xff, alpha: 1), value: 50)
]

Updated
Here is a complete code for your question and additional requirement as requested.

struct Segment {

    // the color of a given segment
    var color: UIColor

    // the value of a given segment – will be used to automatically calculate a ratio
    var value: CGFloat
}

class SliceView: UIView {

    /// An array of structs representing the segments of the pie chart
    var segments = [Segment]() {
        didSet {
            totalValue = segments.reduce(0) { $0 + $1.value }
            setupLabels()
            setNeedsDisplay() // re-draw view when the values get set
            layoutLabels();
        }
    }

    private var totalValue: CGFloat = 1;
    private var labels: [UILabel] = []




    override init(frame: CGRect) {
        super.init(frame: frame)
        isOpaque = false // when overriding drawRect, you must specify this to maintain transparency.
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func draw(_ rect: CGRect) {

        let anglePI2 = (CGFloat.pi * 2)
        let center = CGPoint.init(x: bounds.size.width / 2, y: bounds.size.height / 2)
        let radius = min(bounds.size.width, bounds.size.height) / 2;

        let lineWidth: CGFloat = 1;

        let ctx = UIGraphicsGetCurrentContext()
        ctx?.setLineWidth(lineWidth)


        var currentAngle: CGFloat = 0

        if totalValue <= 0 {
            totalValue = 1
        }

        let iRange = 0 ..< segments.count
        for i in iRange {
            let segment = segments[i]
            // calculate percent
            let percent = segment.value / totalValue

            let angle = anglePI2 * percent

            ctx?.beginPath()
            ctx?.move(to: center)
            ctx?.addArc(center: center, radius: radius - lineWidth, startAngle: currentAngle, endAngle: currentAngle + angle, clockwise: false)
            ctx?.closePath()

            ctx?.setFillColor(segment.color.cgColor)
            ctx?.fillPath()

            ctx?.beginPath()
            ctx?.move(to: center)
            ctx?.addArc(center: center, radius: radius - (lineWidth / 2), startAngle: currentAngle, endAngle: currentAngle + angle, clockwise: false)
            ctx?.closePath()

            ctx?.setStrokeColor(UIColor.white.cgColor)
            ctx?.strokePath()

            currentAngle += angle
        }


    }

    override func layoutSubviews() {
        super.layoutSubviews()
        self.layoutLabels()
    }
    private func setupLabels() {
        var diff = segments.count - labels.count;
        if diff >= 0 {
            for _ in 0 ..< diff {
                let lbl = UILabel()
                self.addSubview(lbl)
                labels.append(lbl)
            }
        } else { // diff < 0 (minus values)
            // loop until diff is 0
            //
            while diff != 0 {
                var lbl: UILabel!
                // if there is no more labels to remove
                // break the loop
                if labels.count <= 0 {
                    break;
                }
                // get the last label
                lbl = labels.removeLast()
                if lbl.superview != nil {
                    lbl.removeFromSuperview()
                }
                // increment the minus value by 1
                diff += 1;
            }
        }

        for i in 0 ..< segments.count {
            let lbl = labels[i]
            lbl.textColor = UIColor.white
            // Change here for your text display
            // I currently display percent of each pies
            lbl.text = String.init(format: "%0.1f", segments[i].value)
            lbl.font = UIFont.init(name: "ArialRoundedMT-Bold", size: 12)
        }
    }
    func layoutLabels() {
        let anglePI2 = CGFloat.pi * 2
        let center = CGPoint.init(x: bounds.size.width / 2, y: bounds.size.height / 2)
        let radius = min(bounds.size.width / 2, bounds.size.height / 2) / 2

        var currentAngle: CGFloat = 0;
        let iRange = 0 ..< labels.count
        for i in iRange {
            let lbl = labels[i]
            let percent = segments[i].value / totalValue

            let intervalAngle = anglePI2 * percent;
            lbl.frame = .zero;
            lbl.sizeToFit()

            let x = center.x + radius * cos(currentAngle + (intervalAngle / 2))
            let y = center.y + radius * sin(currentAngle + (intervalAngle / 2))
            lbl.center = CGPoint.init(x: x, y: y)

            currentAngle += intervalAngle

        }
    }

}

The result is
enter image description here

Upvotes: 3

Related Questions