I am trying to create a donut pie chart which has gradient colors.
This is how I am trying to achieve this
struct Pie: View {
@State private var slices: [(Double, LinearGradient)]
var goldValue: Double
var silverValue: Double
var platinumValue: Double
var palladiumValue: Double
init(goldValue: Double, silverValue: Double, platinumValue: Double, palladiumValue: Double) {
self._slices = State(initialValue: Pie.calculateSlices(goldValue: goldValue, silverValue: silverValue, platinumValue: platinumValue, palladiumValue: palladiumValue))
self.goldValue = goldValue
self.silverValue = silverValue
self.platinumValue = platinumValue
self.palladiumValue = palladiumValue
}
private static func calculateSlices(goldValue: Double, silverValue: Double, platinumValue: Double, palladiumValue: Double) -> [(Double, LinearGradient)] {
let sum = goldValue + silverValue + platinumValue + palladiumValue
let goldGradient = LinearGradient(gradient: Gradient(colors: [.yellow, .orange]), startPoint: .topLeading, endPoint: .bottomTrailing)
let silverGradient = LinearGradient(gradient: Gradient(colors: [.gray, .blue]), startPoint: .topLeading, endPoint: .bottomTrailing)
let platinumGradient = LinearGradient(gradient: Gradient(colors: [.green, .purple]), startPoint: .topLeading, endPoint: .bottomTrailing)
let palladiumGradient = LinearGradient(gradient: Gradient(colors: [.white, .pink]), startPoint: .topLeading, endPoint: .bottomTrailing)
return [
(sum != 0 ? goldValue / sum : 0, goldGradient),
(sum != 0 ? silverValue / sum : 0, silverGradient),
(sum != 0 ? platinumValue / sum : 0, platinumGradient),
(sum != 0 ? palladiumValue / sum : 0, palladiumGradient)
]
}
var body: some View {
Canvas { context, size in
// Start Donut
let donut = Path { p in
let thickness: CGFloat = 20
p.addEllipse(in: CGRect(origin: .zero, size: size))
p.addEllipse(in: CGRect(
x: thickness,
y: thickness,
width: size.width - 2 * thickness,
height: size.height - 2 * thickness
))
}
context.clip(to: donut, style: .init(eoFill: true))
// End Donut
context.translateBy(x: size.width * 0.5, y: size.height * 0.5)
var pieContext = context
pieContext.rotate(by: .degrees(-90))
let radius = min(size.width, size.height) * 0.48
var startAngle = Angle.zero
for (value, gradient) in slices {
let angle = Angle(degrees: 360 * value)
let endAngle = startAngle + angle
let path = Path { p in
p.move(to: .zero)
p.addArc(center: .zero, radius: radius, startAngle: startAngle + Angle(degrees: 0) / 2, endAngle: endAngle, clockwise: false)
p.closeSubpath()
}
pieContext.fill(path, with: gradient)
startAngle = endAngle
}
}
.aspectRatio(contentMode: .fit)
.onAppear {
slices = Pie.calculateSlices(goldValue: goldValue, silverValue: silverValue, platinumValue: platinumValue, palladiumValue: palladiumValue)
}
.onChange(of: goldValue) { newValue in
slices = Pie.calculateSlices(goldValue: newValue, silverValue: silverValue, platinumValue: platinumValue, palladiumValue: palladiumValue)
}
.onChange(of: silverValue) { newValue in
slices = Pie.calculateSlices(goldValue: goldValue, silverValue: newValue, platinumValue: platinumValue, palladiumValue: palladiumValue)
}
.onChange(of: platinumValue) { newValue in
slices = Pie.calculateSlices(goldValue: goldValue, silverValue: silverValue, platinumValue: newValue, palladiumValue: palladiumValue)
}
.onChange(of: palladiumValue) { newValue in
slices = Pie.calculateSlices(goldValue: goldValue, silverValue: silverValue, platinumValue: platinumValue, palladiumValue: newValue)
}
}
}
In the line
pieContext.fill(path, with: gradient)
I get this error
Cannot convert value of type 'LinearGradient' to expected argument type 'GraphicsContext.Shading'
I originally had this code of the donut pie chart which works perfectly fine but it doesn't use gradient colors.
struct Pie: View {
@State private var slices: [(Double, Color)]
var goldValue: Double
var silverValue: Double
var platinumValue: Double
var palladiumValue: Double
init(goldValue: Double, silverValue: Double, platinumValue: Double, palladiumValue: Double) {
self._slices = State(initialValue: Pie.calculateSlices(goldValue: goldValue, silverValue: silverValue, platinumValue: platinumValue, palladiumValue: palladiumValue))
self.goldValue = goldValue
self.silverValue = silverValue
self.platinumValue = platinumValue
self.palladiumValue = palladiumValue
}
private static func calculateSlices(goldValue: Double, silverValue: Double, platinumValue: Double, palladiumValue: Double) -> [(Double, Color)] {
let sum = goldValue + silverValue + platinumValue + palladiumValue
return [
(sum != 0 ? goldValue / sum : 0, Color.yellow),
(sum != 0 ? silverValue / sum : 0, Color.gray),
(sum != 0 ? platinumValue / sum : 0, Color.green),
(sum != 0 ? palladiumValue / sum : 0, Color.white)
]
}
var body: some View {
Canvas { context, size in
// Start Donut
let donut = Path { p in
let thickness: CGFloat = 20
p.addEllipse(in: CGRect(origin: .zero, size: size))
p.addEllipse(in: CGRect(
x: thickness,
y: thickness,
width: size.width - 2 * thickness,
height: size.height - 2 * thickness
))
}
context.clip(to: donut, style: .init(eoFill: true))
// End Donut
context.translateBy(x: size.width * 0.5, y: size.height * 0.5)
var pieContext = context
pieContext.rotate(by: .degrees(-90))
let radius = min(size.width, size.height) * 0.48
var startAngle = Angle.zero
for (value, color) in slices {
let angle = Angle(degrees: 360 * value)
let endAngle = startAngle + angle
let path = Path { p in
p.move(to: .zero)
p.addArc(center: .zero, radius: radius, startAngle: startAngle + Angle(degrees: 0) / 2, endAngle: endAngle, clockwise: false)
p.closeSubpath()
}
pieContext.fill(path, with: .color(color))
startAngle = endAngle
}
}
.aspectRatio(contentMode: .fit)
.onAppear {
slices = Pie.calculateSlices(goldValue: goldValue, silverValue: silverValue, platinumValue: platinumValue, palladiumValue: palladiumValue)
}
.onChange(of: goldValue) { newValue in
slices = Pie.calculateSlices(goldValue: newValue, silverValue: silverValue, platinumValue: platinumValue, palladiumValue: palladiumValue)
}
.onChange(of: silverValue) { newValue in
slices = Pie.calculateSlices(goldValue: goldValue, silverValue: newValue, platinumValue: platinumValue, palladiumValue: palladiumValue)
}
.onChange(of: platinumValue) { newValue in
slices = Pie.calculateSlices(goldValue: goldValue, silverValue: silverValue, platinumValue: newValue, palladiumValue: palladiumValue)
}
.onChange(of: palladiumValue) { newValue in
slices = Pie.calculateSlices(goldValue: goldValue, silverValue: silverValue, platinumValue: platinumValue, palladiumValue: newValue)
}
}
}
I would just like the similar functionality with gradient colours.
GraphicsContext
doesn't take ShapeStyle
s like a lot of the view modifiers do. It takes a GraphicsContext.Shading
, which can be created by using one of the factory methods. For example, in your last code snippet, you've used .color(color)
to create a solid color shading. You can't directly pass color
to it.
There is a factory method for creating linear gradients too: linearGradient
. Unlike the ShapeStyle
, this method takes the start and end points in the coordinate space of the canvas, instead of UnitPoint
s.
I would first change calculateSlices
to return a [(Double, Gradient)]
, and change the type of the slices
state accordingly. So you'd do:
let goldGradient = Gradient(colors: [.yellow, .orange])
let silverGradient = Gradient(colors: [.gray, .blue])
let platinumGradient = Gradient(colors: [.green, .purple])
let palladiumGradient = Gradient(colors: [.white, .pink])
You would calculate the start and end points in the Canvas
closure.
Depending on what you want, you can either do:
// use the top leading and bottom trailing points of the path's bound rect
let start = CGPoint(x: arc.boundingRect.maxX, y: arc.boundingRect.minY)
let end = CGPoint(x: arc.boundingRect.minX, y: arc.boundingRect.maxY)
pieContext.fill(path, with: .linearGradient(gradient, startPoint: start, endPoint: end))
Or
// use the top leading and bottom trailing points of the canvas
let start = CGPoint(x: -size.width / 2, y: -size.height / 2)
// also need to rotate these points since the context is rotated
.applying(.init(rotationAngle: -.pi / 2))
let end = CGPoint(x: size.width / 2, y: size.height / 2)
.applying(.init(rotationAngle: -.pi / 2))
pieContext.fill(path, with: .linearGradient(gradient, startPoint: start, endPoint: end))