用切片空间制作饼图(饼图中每种颜色之间的间隔)

时间:2018-07-20 08:49:48

标签: swift

Pie Chart Sample

我正在从事一个项目,在该项目中,我需要像上面那样实现饼图。我需要在切片之间添加一个空格作为图片。需要在带有弧的切片中留出空间,并在百分比中包含值。

我尝试实现它,但无法成功。我使用错误的获取弧形。

请帮助我。谢谢。

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)"
    }
}

///定义饼图的一部分

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
        }
    }
}

1 个答案:

答案 0 :(得分:2)

在您的draw(_:)中实施此操作。

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
    }
}

说明
-计算等于360度的pi
-计算中心。
-计算块的半径。
-计算[段]的totalValue。在计算圆中切片的百分比时使用。
-遍历线段,计算当前线段的百分比,制作用于填充的弧(半径-lineWidth),重新绘制用于描边路径的弧(半径-lineWidth / 2)。

这是结果enter image description here

在我的视图控制器中,我添加了数据(供您参考)。像这样

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)
]

已更新
这是您的问题和要求的其他要求的完整代码。

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

        }
    }

}

结果是
enter image description here