Skip to content

Instantly share code, notes, and snippets.

@Brennanium
Created February 13, 2023 22:24
Show Gist options
  • Select an option

  • Save Brennanium/16c36f4ace74992f3107ca6676ec7195 to your computer and use it in GitHub Desktop.

Select an option

Save Brennanium/16c36f4ace74992f3107ca6676ec7195 to your computer and use it in GitHub Desktop.
Making a doughnut chart with rounded corners and variable padding for the slices
import SwiftUI
struct DoughnutChartCell: InsettableShape {
let startAngle: Angle
let endAngle: Angle
private let innerRadiusBuilder: (CGFloat) -> CGFloat
private var insetAmount: CGFloat = 0
init(startAngle: Double, endAngle: Double, innerRadius: Double) {
self.startAngle = Angle(degrees: 360 - startAngle)
self.endAngle = Angle(degrees: 360 - endAngle)
self.innerRadiusBuilder = { _ in
CGFloat(innerRadius)
}
}
init(startAngle: Double, endAngle: Double, innerRadiusRatio: Double = 0.5) {
self.startAngle = Angle(degrees: 360 - startAngle)
self.endAngle = Angle(degrees: 360 - endAngle)
self.innerRadiusBuilder = { radius in
radius * innerRadiusRatio
}
}
func inset(by amount: CGFloat) -> some InsettableShape {
var shape = self
shape.insetAmount += amount
return shape
}
func path(in rect: CGRect) -> Path {
let center = CGPoint(x: (rect.origin.x + rect.width)/2, y: (rect.origin.y + rect.height)/2)
let radius = min(center.x, center.y)
let innerRadius = innerRadiusBuilder(radius)
let radiusInset = radius - insetAmount - outerPadding
let innerRadiusInset = innerRadius + insetAmount + innerPadding
let startAngleInset = startAngle - Angle(radians: (insetAmount + clockwisePadding) / radiusInset)
let innerStartAngleInset = startAngle - Angle(radians: (insetAmount + clockwisePadding) / innerRadiusInset)
let endAngleInset = endAngle + Angle(radians: (insetAmount + counterClockwisePadding) / radiusInset)
let innerEndAngleInset = endAngle + Angle(radians: (insetAmount + counterClockwisePadding) / innerRadiusInset)
let firstInnerCornerX = center.x + (innerRadiusInset * cos(innerEndAngleInset.radians))
let firstInnerCornerY = center.y + (innerRadiusInset * sin(innerEndAngleInset.radians))
let firstInnerCorner = CGPoint(x: firstInnerCornerX, y: firstInnerCornerY)
let path = Path { p in
p.addArc(center: center,
radius: radiusInset,
startAngle: startAngleInset,
endAngle: endAngleInset,
clockwise: true)
p.addLine(to: firstInnerCorner)
p.addArc(center: center,
radius: innerRadiusInset,
startAngle: innerEndAngleInset,
endAngle: innerStartAngleInset,
clockwise: false)
p.closeSubpath()
}
return path
}
private var outerPadding: CGFloat = 0.0
private var innerPadding: CGFloat = 0.0
private var clockwisePadding: CGFloat = 0.0
private var counterClockwisePadding: CGFloat = 0.0
private let defaultPadding: CGFloat = 15.0
func padding(_ edges: Self.Edge.Set = .all, _ amount: CGFloat? = nil) -> Self {
var shape = self
let amount = amount ?? defaultPadding
for option: Self.Edge.Set in [.inner, .outer, .clockwise, .counterClockwise] {
guard edges.contains(option) else { continue }
switch option {
case .outer:
shape.outerPadding += amount
case .inner:
shape.innerPadding += amount
case .clockwise:
shape.clockwisePadding += amount
case .counterClockwise:
shape.counterClockwisePadding += amount
default:
continue
}
}
return shape
}
enum Edge: Int8, CaseIterable, Hashable {
case outer
case inner
case clockwise
case counterClockwise
struct Set: OptionSet {
let rawValue: Int8
static let outer = Set(rawValue: 1 << 0)
static let inner = Set(rawValue: 1 << 1)
static let clockwise = Set(rawValue: 1 << 2)
static let counterClockwise = Set(rawValue: 1 << 3)
static let innerOuter: Self = [.inner, .outer]
static let sides: Self = [.clockwise, .counterClockwise]
static let all: Self = [.inner, .outer, .clockwise, .counterClockwise]
}
}
}
struct ContentView: View {
var body: some View {
VStack {
GeometryReader { geometry in
ZStack {
// RED CELL
// outline
DoughnutChartCell(startAngle: 0, endAngle: 45)
.stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
.foregroundColor(.purple)
// border
DoughnutChartCell(startAngle: 0, endAngle: 45)
.padding(.sides, 3)
.strokeBorder(style: StrokeStyle(lineWidth: 30, lineCap: .round, lineJoin: .round))
.foregroundColor(.pink)
// YELLOW CELL
// outline
DoughnutChartCell(startAngle: 45, endAngle: 100)
.stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
.foregroundColor(.orange)
// border
DoughnutChartCell(startAngle: 45, endAngle: 100)
.padding(.sides, 3)
.strokeBorder(style: StrokeStyle(lineWidth: 30, lineCap: .round, lineJoin: .round))
.foregroundColor(.yellow)
// GREEN CELL
// outline
DoughnutChartCell(startAngle: 100, endAngle: 220)
.stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
.foregroundColor(.teal)
// background
DoughnutChartCell(startAngle: 100, endAngle: 220)
.padding(.all, 25)
.foregroundColor(.teal)
// border
DoughnutChartCell(startAngle: 100, endAngle: 220)
.padding(.sides, 3)
.strokeBorder(style: StrokeStyle(lineWidth: 30, lineCap: .round, lineJoin: .round))
.foregroundColor(.green)
// icon
let radius = geometry.size.width / 2
let middleRadius = (radius + radius/2) / 2
let x = middleRadius * cos(Angle(degrees: (220 + 100)/2).radians)
let y = middleRadius * sin(Angle(degrees: (220 + 100)/2).radians)
Image(systemName: "leaf")
.offset(x: x, y: -y)
// CENTER TEXT
Text("Hello World")
}
}
.padding(50)
.background(Color.blue)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment