I wonder how can we create a pie chart with rounded edges and spaces between pie as shown in the photo.
My first approach: I move the pies out of its center point an offset = 10 to make it look like the photo. But It seems like the radius of the biggest pie is smaller than the smaller ones. Then I make a change on Radius, but the spacing a bit weird And since the newCenter point is not in the center of superview, It’s cut off on a side.
outerRadius = outerRadius - offset * 2 * (1 - percentage)
(Percentage is the proportion of pie in the chart)
My second approach: I calculate the center point for each pie instead of moving it out of its original center point. Imagine there’s an empty middle as a circle and a new center point for each pie is in that circle.
The issues still occur with large pies.
The new center point for each slide on my tries:
let middleAngle = ((startAngle + endAngle) / 2.0).toRadians()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let newCenter = CGPoint(x: center.x + cos(middleAngle) * offset, y: center.y + sin(middleAngle) * offset)
Issues with radius and center point | Expected result
Here’s my code https://gist.github.com/phongngo511/dfd416aaad45fc0241cd4526d80d94d6
Hi is this what you're trying to achieve? If so, I think your approach had a couple of issues. Firstly, looking at your code gist, I changed a couple of things from the way you were doing it:
Let me know if anything needs clarification, hope this helps!
import UIKit
class PieChartView: UIView {
var onTouchPie: ((_ sliceIndex: Int) -> ())?
var shouldHighlightPieOnTouch = false
var shouldShowLabels: Bool = false {
didSet { setNeedsLayout() }
}
var labelTextFont = UIFont.systemFont(ofSize: 12) {
didSet { setNeedsLayout() }
}
var labelTextColor = UIColor.black {
didSet { setNeedsLayout() }
}
var shouldShowTextPercentageFromFieFilledFigures = false {
didSet { setNeedsLayout() }
}
var pieGradientColors: [[UIColor]] = [[.red,.red], [.cyan,.cyan], [.green,.green]] {
didSet { setNeedsLayout() }
}
var pieFilledPercentages:[CGFloat] = [1, 1, 1] {
didSet { setNeedsLayout() }
}
//var segments:[CGFloat] = [40, 30, 30] {
var segments:[CGFloat] = [70, 20, 10] {
didSet { setNeedsLayout() }
}
var offset:CGFloat = 15 {
didSet { setNeedsLayout() }
}
var spaceLineColor: UIColor = .white {
didSet { setNeedsLayout() }
}
private var labels: [UILabel] = []
private var labelSize = CGSize(width: 100, height: 50)
private var shapeLayers = [CAShapeLayer]()
private var gradientLayers = [CAGradientLayer]()
override func layoutSubviews() {
super.layoutSubviews()
labels.forEach({$0.removeFromSuperview()})
labels.removeAll()
shapeLayers.forEach({$0.removeFromSuperlayer()})
shapeLayers.removeAll()
gradientLayers.forEach({$0.removeFromSuperlayer()})
gradientLayers.removeAll()
let valueCount = segments.reduce(CGFloat(0), {$0 + $1})
guard pieFilledPercentages.count >= 3, segments.count >= 3, pieGradientColors.count >= 3 , valueCount > 0 else { return }
let radius = min(bounds.width / 2, bounds.height / 2) * 0.9 //KEN CHANGED
var startAngle: CGFloat = 360
let proportions = segments.map({ ($0 / valueCount * 100).rounded()})
for i in 0..<segments.count {
let endAngle = startAngle - proportions[i] / 100 * 360
let path = createPath(from: startAngle, to: endAngle, oRadius: radius, percentage: proportions[i])
//path.close() //KEN CHANGED
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayers.append(shapeLayer)
let gradientLayer = CAGradientLayer()
gradientLayer.colors = pieGradientColors[i].map({$0.cgColor})
if i == 0 {
gradientLayer.locations = [0.5, 1]
} else {
gradientLayer.locations = [0, 0.5]
}
gradientLayer.mask = shapeLayer
gradientLayer.frame = bounds
if proportions[i] != 0 && pieFilledPercentages[i] != 0 {
layer.addSublayer(gradientLayer)
gradientLayers.append(gradientLayer)
}
let label = labelFromPoint(point: getCenterPointOfArc(startAngle: startAngle, endAngle: endAngle), andText: String(format: "%.f", shouldShowTextPercentageFromFieFilledFigures ? pieFilledPercentages[i] * 100 :segments[i]) + "%")
label.isHidden = !shouldShowLabels
if proportions[i] != 0 {
addSubview(label)
labels.append(label)
}
startAngle = endAngle
}
}
private func labelFromPoint(point: CGPoint, andText text: String) -> UILabel {
let label = UILabel(frame: CGRect(origin: point, size: labelSize))
label.font = labelTextFont
label.textColor = labelTextColor
label.text = text
return label
}
private func getCenterPointOfArc(startAngle: CGFloat, endAngle: CGFloat) -> CGPoint {
let oRadius = max(bounds.width / 2, bounds.height / 2) * 0.8
let center = CGPoint(x: oRadius, y: oRadius)
let centerAngle = ((startAngle + endAngle) / 2.0).toRadians()
let arcCenter = CGPoint(x: center.x + oRadius * cos(centerAngle), y: center.y - oRadius * sin(centerAngle))
return CGPoint(x: (center.x + arcCenter.x) / 2, y: (center.y + arcCenter.y) / 2)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first, shouldHighlightPieOnTouch {
shapeLayers.enumerated().forEach { (item) in
if let path = item.element.path, path.contains(touch.location(in: self)) {
item.element.opacity = 1
onTouchPie?(item.offset)
} else {
item.element.opacity = 0.3
}
}
}
super.touchesBegan(touches, with: event)
}
private func highlightLayer(index: Int) {
shapeLayers.enumerated().forEach({$0.element.opacity = $0.offset == index ? 1: 0.3 })
}
private func createPath(from startAngle: CGFloat, to endAngle: CGFloat, oRadius: CGFloat, cornerRadius: CGFloat = 10, percentage: CGFloat) -> UIBezierPath {
let radius: CGFloat = min(bounds.width, bounds.height) / 2.0 - (2.0 * offset)
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let midPointAngle = ((startAngle + endAngle) / 2.0).toRadians() //used to spread the segment away from its neighbours after creation
let startAngle = (360.0 - startAngle).toRadians()
let endAngle = (360.0 - endAngle).toRadians()
let circumference: CGFloat = CGFloat(2.0 * (Double.pi * Double(radius)))
let arcLengthPerDegree = circumference / 360.0 //how many pixels long the outer arc is of the pie chart, per 1° of a pie segment
let pieSegmentOuterCornerRadiusInDegrees: CGFloat = 4.0 //for a given segment (and if it's >4° in size), use up 2 of its outer arc's degrees as rounded corners.
let pieSegmentOuterCornerRadius = arcLengthPerDegree * pieSegmentOuterCornerRadiusInDegrees
let path = UIBezierPath()
//move to the centre of the pie chart, offset by the corner radius (so the corner of the segment can be rounded in a bit)
path.move(to: CGPoint(x: center.x + (cos(startAngle - CGFloat(360).toRadians()) * cornerRadius), y: center.y + (sin(startAngle - CGFloat(360).toRadians()) * cornerRadius)))
//if the size of the pie segment isn't big enough to warrant rounded outer corners along its outer arc, don't round them off
if ((endAngle - startAngle).toDegrees() <= (pieSegmentOuterCornerRadiusInDegrees * 2.0)) {
//add line from centre of pie chart to 1st outer corner of segment
path.addLine(to: CGPoint(x: center.x + (cos(startAngle - CGFloat(360).toRadians()) * radius), y: center.y + (sin(startAngle - CGFloat(360).toRadians()) * radius)))
//add arc for segment's outer edge on pie chart
path.addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
//move down to the centre of the pie chart, leaving room for rounded corner at the end
path.addLine(to: CGPoint(x: center.x + (cos(endAngle - CGFloat(360).toRadians()) * cornerRadius), y: center.y + (sin(endAngle - CGFloat(360).toRadians()) * cornerRadius)))
//add final rounded corner in middle of pie chart
path.addQuadCurve(to: CGPoint(x: center.x + (cos(startAngle - CGFloat(360).toRadians()) * cornerRadius), y: center.y + (sin(startAngle - CGFloat(360).toRadians()) * cornerRadius)), controlPoint: center)
} else { //round the corners on the outer arc
//add line from centre of pie chart to circumference of segment, minus the space needed for the rounded corner
path.addLine(to: CGPoint(x: center.x + (cos(startAngle - CGFloat(360).toRadians()) * (radius - pieSegmentOuterCornerRadius)), y: center.y + (sin(startAngle - CGFloat(360).toRadians()) * (radius - pieSegmentOuterCornerRadius))))
//add rounded corner onto start of outer arc
let firstRoundedCornerEndOnArc = CGPoint(x: center.x + (cos(startAngle + pieSegmentOuterCornerRadiusInDegrees.toRadians() - CGFloat(360).toRadians()) * radius), y: center.y + (sin(startAngle + pieSegmentOuterCornerRadiusInDegrees.toRadians() - CGFloat(360).toRadians()) * radius))
path.addQuadCurve(to: firstRoundedCornerEndOnArc, controlPoint: CGPoint(x: center.x + (cos(startAngle - CGFloat(360).toRadians()) * radius), y: center.y + (sin(startAngle - CGFloat(360).toRadians()) * radius)))
//add arc for segment's outer edge on pie chart
path.addArc(withCenter: center, radius: radius, startAngle: startAngle + pieSegmentOuterCornerRadiusInDegrees.toRadians(), endAngle: endAngle - pieSegmentOuterCornerRadiusInDegrees.toRadians(), clockwise: true)
//add rounded corner onto end of outer arc
let secondRoundedCornerEndOnLine = CGPoint(x: center.x + (cos(endAngle - CGFloat(360).toRadians()) * (radius - pieSegmentOuterCornerRadius)), y: center.y + (sin(endAngle - CGFloat(360).toRadians()) * (radius - pieSegmentOuterCornerRadius)))
path.addQuadCurve(to: secondRoundedCornerEndOnLine, controlPoint: CGPoint(x: center.x + (cos(endAngle - CGFloat(360).toRadians()) * radius), y: center.y + (sin(endAngle - CGFloat(360).toRadians()) * radius)))
//add line back to centre point of pie chart, leaving room for rounded corner at the end
path.addLine(to: CGPoint(x: center.x + (cos(endAngle - CGFloat(360).toRadians()) * cornerRadius), y: center.y + (sin(endAngle - CGFloat(360).toRadians()) * cornerRadius)))
//add final rounded corner in middle of pie chart
path.addQuadCurve(to: CGPoint(x: center.x + (cos(startAngle - CGFloat(360).toRadians()) * cornerRadius), y: center.y + (sin(startAngle - CGFloat(360).toRadians()) * cornerRadius)), controlPoint: center)
}
path.close()
//spread the segments out around the pie chart centre
path.apply(CGAffineTransform(translationX: cos(midPointAngle) * offset, y: -sin(midPointAngle) * offset))
return path
}
}
extension CGFloat {
func toRadians() -> CGFloat {
return self * CGFloat(Double.pi) / 180.0
}
func toDegrees() -> CGFloat {
return self / (CGFloat(Double.pi) / 180.0)
}
}