SwiftUI Add Bottom Gradient To Line Graph - swift

I want to add a bottom gradient in my line graph like in the picture. I have the lines plotted out but I am not sure how to actually add the gradient. I was able to do this in UIKit but am not sure how to replicate it for SwiftUI.
SwiftUI Line Plot View:
var endingBalanceChart: some View {
GeometryReader { geometry in
Path { path in
for index in viewModel.endingBalance.indices {
let xPosition: CGFloat = geometry.size.width / CGFloat(viewModel.endingBalance.count) * CGFloat(index + 1)
let yAxis: CGFloat = maxY - minY
let maxYPosition: CGFloat = (1 - CGFloat((Double(viewModel.endingBalance[index].y) - minY) / yAxis)) * geometry.size.height
let yPosition: CGFloat = index == 0 ? 200 : maxYPosition
if index == 0 {
path.move(to: CGPoint(x: 0, y: yPosition))
}
path.addLine(to: CGPoint(x: xPosition, y: yPosition))
}
}
.stroke(Color.blue, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round))
}
}
I tried converting the path in to a UIBezierPath and trying my UIKit implementation but no luck
This was my UIKit implementation:
func addGradient(path: UIBezierPath, hexString: String){
let color = UIColor(hexString: hexString).withAlphaComponent(0.4).cgColor
guard let clippingPath = path.copy() as? UIBezierPath else { return }
clippingPath.addLine(to: CGPoint(x: self.bounds.width, y: self.bounds.height))
clippingPath.addLine(to: CGPoint(x: 0, y: bounds.height))
clippingPath.close()
clippingPath.addClip()
let colors = [color, UIColor.clear.cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let colorLocations: [CGFloat] = [0.0, 1.0]
guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: colorLocations) else { return }
guard let context = UIGraphicsGetCurrentContext() else { return }
let startPoint = CGPoint(x: 1, y: 1)
let endPoint = CGPoint(x: 1, y: bounds.maxY)
context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: .drawsAfterEndLocation)
}

Here would be a pure SwiftUI implementation.
I suggest to convert the var into an own struct that conforms to Shape protocol. Then you can use it for both stroke and fill background.
This has the positive side effect that you don't need a GeometryReader any more, as Shape provides you with the drawing rectangle with func path(in rect: CGRect).
The result looks like this:
let endingBalance: [Double] = [0, 1, 2, 4, 7, 11, 16, 22, 29, 37, 46, 56] // dummy data
struct ContentView: View {
var body: some View {
VStack {
Text("Chart")
EndingBalanceChart()
.stroke(Color.blue, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round)) // line
.background(
EndingBalanceChart(isBackground: true)
.fill(.linearGradient(colors: [.cyan, .clear], startPoint: .top, endPoint: .bottom)) // background fill
)
.frame(height: 200)
.padding()
}
}
}
struct EndingBalanceChart: Shape { // chnaged var to a Shape struct
var isBackground: Bool = false
func path(in rect: CGRect) -> Path {
Path { path in
for index in endingBalance.indices {
let xPosition: CGFloat = rect.width / CGFloat(endingBalance.count) * CGFloat(index + 1)
let maxY = endingBalance.max() ?? 0
let minY = endingBalance.min() ?? 0
let yAxis: CGFloat = maxY - minY
let yPosition: CGFloat = (1 - CGFloat((Double(endingBalance[index]) - minY) / yAxis)) * rect.height
if index == 0 {
path.move(to: CGPoint(x: 0, y: rect.height))
}
path.addLine(to: CGPoint(x: xPosition, y: yPosition))
}
if isBackground { // this is needed so the backkground shape is filled correctly (closing the shape)
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
}
}
}
}

Here is an approach:
let amountPaid: [Double] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] // dummy data
let endingBalance: [Double] = [0, 1, 2, 4, 7, 11, 16, 22, 29, 37, 46, 56] // dummy data
struct ContentView: View {
#State private var value: Double = 0
var body: some View {
VStack {
Text("Chart")
CurveChart(data: endingBalance)
.stroke(Color.blue, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round)) // line
.background(
CurveChart(data: endingBalance, isBackground: true)
.fill(.linearGradient(colors: [.cyan, .clear], startPoint: .top, endPoint: .bottom)) // background fill
)
.frame(height: 200)
.overlay(alignment: .topTrailing, content: {
plotPoint(data: endingBalance, index: Int(value))
.fill(.blue)
})
CurveChart(data: amountPaid)
.stroke(Color.blue, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round)) // line
.background(
CurveChart(data: amountPaid, isBackground: true)
.fill(.linearGradient(colors: [.green, .clear], startPoint: .top, endPoint: .bottom)) // background fill
)
.frame(height: 60)
.overlay(alignment: .topTrailing, content: {
plotPoint(data: amountPaid, index: Int(value))
.fill(.green)
})
.padding(.bottom)
Slider(value: $value, in: 0...Double(endingBalance.count-1), step: 1.0)
}
.padding()
}
}
struct CurveChart: Shape { // chnaged var to a Shape struct
let data: [Double]
var isBackground: Bool = false
func path(in rect: CGRect) -> Path {
Path { path in
for index in data.indices {
let xPosition: CGFloat = rect.width / CGFloat(data.count-1) * CGFloat(index)
let maxY = data.max() ?? 0
let minY = data.min() ?? 0
let yAxis: CGFloat = maxY - minY
let yPosition: CGFloat = (1 - CGFloat((Double(data[index]) - minY) / yAxis)) * rect.height
if index == 0 {
path.move(to: CGPoint(x: 0, y: rect.height))
}
path.addLine(to: CGPoint(x: xPosition, y: yPosition))
}
if isBackground { // this is needed so the backkground shape is filled correctly (closing the shape)
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
}
}
}
}
struct plotPoint: Shape { // chnaged var to a Shape struct
let data: [Double]
let index: Int
let size = 20.0
func path(in rect: CGRect) -> Path {
let xStep = rect.width / Double(data.count-1)
let yStep = rect.height / (data.max() ?? 0)
let xCenter = Double(index) * xStep
let yCenter = rect.height - yStep * data[index]
var path = Path()
path.addEllipse(in: CGRect(x: xCenter - size/2, y: yCenter - size/2, width: size, height: size))
return path
}
}

Related

Add filled circles (markers) at SwiftUI path points

I'm trying to draw a line with markers at each point by using a Shape view in SwiftUI. I want the line to have a filled circle at each CGPoint on the line. The closest to this that I've gotten is by adding an arc at each point. Instead of using the arc, how can I add a Circle() shape at each point? I'm open to other approaches to accomplish this. My only requirements are to use SwiftUI and have the ability to interact with the markers.
import SwiftUI
struct LineShape: Shape {
let values: [Double]
func path(in rect: CGRect) -> Path {
let xStep = rect.width / CGFloat(values.count - 1)
var path = Path()
path.move(to: CGPoint(x: 0.0, y: (1 - values[0]) * Double(rect.height)))
for i in 1..<values.count {
let pt = CGPoint(x: Double(i) * Double(xStep), y: (1 - values[i]) * Double(rect.height))
path.addLine(to: pt)
path.addArc(center: pt, radius: 8, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 360), clockwise: false)
}
return path
}
}
struct ContentView: View {
var body: some View {
LineShape(values: [0.2, 0.4, 0.3, 0.8, 0.5])
.stroke(.red, lineWidth: 2.0)
.padding()
.frame(width: 400, height: 300)
}
}
You can make two different shapes, one for the line and one for the markers, and overlay them. Then you can also control their coloring individually:
struct LineShape: Shape {
let values: [Double]
func path(in rect: CGRect) -> Path {
let xStep = rect.width / CGFloat(values.count - 1)
var path = Path()
path.move(to: CGPoint(x: 0.0, y: (1 - values[0]) * Double(rect.height)))
for i in 1..<values.count {
let pt = CGPoint(x: Double(i) * Double(xStep), y: (1 - values[i]) * Double(rect.height))
path.addLine(to: pt)
}
return path
}
}
struct MarkersShape: Shape {
let values: [Double]
func path(in rect: CGRect) -> Path {
let xStep = rect.width / CGFloat(values.count - 1)
var path = Path()
for i in 1..<values.count {
let pt = CGPoint(x: Double(i) * Double(xStep), y: (1 - values[i]) * Double(rect.height))
path.addEllipse(in: CGRect(x: pt.x - 8, y: pt.y - 8, width: 16, height: 16))
}
return path
}
}
struct ContentView: View {
var body: some View {
LineShape(values: [0.2, 0.4, 0.3, 0.8, 0.5])
.stroke(.red, lineWidth: 2.0)
.overlay(
MarkersShape(values: [0.2, 0.4, 0.3, 0.8, 0.5])
.fill(.blue)
)
.frame(width: 350, height: 300)
}
}
According to previous posts and comments here is my solution:
First there is need to use extension for filling and setting shape stroke:
extension Shape {
public func fill<Shape: ShapeStyle>(_ fillContent: Shape, strokeColor: Color, lineWidth: CGFloat) -> some View {
ZStack {
self.fill(fillContent)
self.stroke(strokeColor, lineWidth: lineWidth)
}
}
}
Another thing is Scaler struct used to make scaling calculations:
struct Scaler {
let bounds: CGPoint
let maxVal: Double
let minVal: Double
let valuesCount: Int
var xFactor: CGFloat {
valuesCount < 2 ? bounds.x : bounds.x / CGFloat(valuesCount - 1)
}
var yFactor: CGFloat {
bounds.y / maxVal
}
}
Now it's time for real chart shape drawing:
struct ChartLineDotsShape: Shape {
let values: [Double]
let dotRadius: CGFloat
func path(in rect: CGRect) -> Path {
guard let maxVal = values.max(), let minVal = values.min() else { return Path() }
let scaler = Scaler(bounds: CGPoint(x: rect.width, y: rect.height), maxVal: maxVal, minVal: minVal, valuesCount: values.count)
return Path { path in
var valuesIterator = values.makeIterator()
let dx = scaler.xFactor
var x = 0.0
guard let y = valuesIterator.next() else { return }
path.move(to: CGPoint(x: x, y: calculate(y, scaler: scaler)))
draw(point: CGPoint(x: x, y: calculate(y, scaler: scaler)), on: &path)
x += dx
while let y = valuesIterator.next() {
draw(point: CGPoint(x: x, y: calculate(y, scaler: scaler)), on: &path)
x += dx
}
}
}
private func calculate(_ value: CGFloat, scaler: Scaler) -> CGFloat {
scaler.bounds.y - value * scaler.yFactor
}
private func draw(point: CGPoint, on path: inout Path) {
path.addLine(to: CGPoint(x: point.x, y: point.y))
path.addEllipse(in: CGRect(x: point.x - dotRadius * 0.5, y: point.y - dotRadius * 0.5, width: dotRadius, height: dotRadius))
path.move(to: CGPoint(x: point.x, y: point.y))
}
}
If there is single iteration loop lines dots drawing shape we can try to use it in some view:
public struct ChartView: View {
public var body: some View {
ChartLineDotsShape(values: [0.3, 0.5, 1.0, 2.0, 2.5, 2.0, 3.0, 6.0, 2.0, 1.5, 2.0], dotRadius: 4.0)
.fill(Color.blue, strokeColor: Color.blue, lineWidth: 1.0)
.shadow(color: Color.blue, radius: 2.0, x: 0.0, y: 0.0)
.frame(width: 320.0, height: 200.0)
.background(Color.blue.opacity(0.1))
.clipped()
}
}
Finally preview setup:
struct ChartView_Previews: PreviewProvider {
static var previews: some View {
ChartView()
.preferredColorScheme(.dark)
}
}
and viola:

Current time is not displayed in the view

I have just recently started using Swiftui and have the following question:
I would like to convert the current time to an analog view. But when I use the code below, I always get an error message at the angle. What is the reason that it does not work?
struct Hand: Shape {
let inset: CGFloat
let angle: Angle
func path(in rect: CGRect) -> Path {
let rect = rect.insetBy(dx: inset, dy: inset)
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.midY))
path.addRoundedRect(in: CGRect(x: rect.midX - 4, y: rect.midY - 4, width: 8, height: 8), cornerSize: CGSize(width: 8, height: 8))
path.move(to: CGPoint(x: rect.midX, y: rect.midY))
path.addLine(to: position(for: CGFloat(angle.radians), in: rect))
return path
}
private func position(for angle: CGFloat, in rect: CGRect) -> CGPoint {
let angle = angle - (.pi/2)
let radius = min(rect.width, rect.height)/2
let xPosition = rect.midX + (radius * cos(angle))
let yPosition = rect.midY + (radius * sin(angle))
return CGPoint(x: xPosition, y: yPosition)
}
}
struct TickHands: View {
#State private var currentDate = Date()
let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
Hand(inset: 50, angle: currentDate.hourAngle)
.stroke(lineWidth: 4)
.foregroundColor(.black)
Hand(inset: 22, angle: currentDate.minuteAngle)
.stroke(lineWidth: 4)
.foregroundColor(.black)
Hand(inset: 10, angle: currentDate.secondAngle)
.stroke(lineWidth: 2)
.foregroundColor(.gray)
}
.onReceive(timer) { (input) in
self.currentDate = input
}}}
Based on the link you provided, It looks like the author of the article did not provide some extensions.
Add the following extensions to your code (preferable in a new file), and you should be good 🚀:
extension Date {
var hourAngle: Angle {
return Angle (degrees: (360 / 12) * (self.hour + self.minutes / 60))
}
var minuteAngle: Angle {
return Angle(degrees: (self.minutes * 360 / 60))
}
var secondAngle: Angle {
return Angle (degrees: (self.seconds * 360 / 60))
}
}
extension Date {
var hour: Double {
return Double(Calendar.current.component(.hour, from: self))
}
var minutes: Double {
return Double(Calendar.current.component(.minute, from: self))
}
var seconds: Double {
return Double(Calendar.current.component(.second, from: self))
}
}

Draw Shape over another Shape

Problem :
I cannot draw a shape on another shape.
What I am trying to achieve :
Draw circles on the line.
Anyway, the circle is shifting the line. I didn't find a way to make it as swift UI seems relatively new. I am currently learning swift and I prefer swift UI rater than storyboard.
If circle and line are different struct, this is because I want to reuse the shape later on.
There is the code :
import SwiftUI
public var PointArray = [CGPoint]()
public var PointArrayInit:Bool = false
struct Arc: Shape {
var startAngle: Angle
var endAngle: Angle
var clockwise: Bool
var centerCustom:CGPoint
func path(in rect: CGRect) -> Path {
let rotationAdjustment = Angle.degrees(90)
let modifiedStart = startAngle - rotationAdjustment
let modifiedEnd = endAngle - rotationAdjustment
var path = Path()
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: 20, startAngle: modifiedStart, endAngle: modifiedEnd, clockwise: !clockwise)
return path
}
}
struct CurveCustomInit: Shape {
private var Divider:Int = 10
func path(in rect: CGRect) -> Path {
var path = Path()
let xStep:CGFloat = DrawingZoneWidth / CGFloat(Divider)
let yStep:CGFloat = DrawingZoneHeight / 2
var xStepLoopIncrement:CGFloat = 0
path.move(to: CGPoint(x: 0, y: yStep))
for _ in 0...Divider {
let Point:CGPoint = CGPoint(x: xStepLoopIncrement, y: yStep)
PointArray.append(Point)
path.addLine(to: Point)
xStepLoopIncrement += xStep
}
PointArrayInit = true
return (path)
}
}
struct TouchCurveBasic: View {
var body: some View {
if !PointArrayInit {
Arc(startAngle: .degrees(0), endAngle: .degrees(360), clockwise: true, centerCustom: CGPoint(x: 50, y: 400))
.stroke(Color.blue, lineWidth: 4)
CurveCustomInit()
.stroke(Color.red, style: StrokeStyle(lineWidth: 10, lineCap: .round, lineJoin: .round))
.frame(width: 300, height: 300)
} else {
}
}
}
struct TouchCurveBasic_Previews: PreviewProvider {
static var previews: some View {
TouchCurveBasic()
}
}
There is what I get :
Here is an other way for you, you can limit the size of drawing with giving a frame or you can use the available size of view without limiting it or even you can use the current limit coming from parent and updated it, like i did on drawing the Line. The method that I used was overlay modifier.
struct ContentView: View {
var body: some View {
ArcView(radius: 30.0)
.stroke(lineWidth: 10)
.foregroundColor(.blue)
.frame(width: 60, height: 60)
.overlay(LineView().stroke(lineWidth: 10).foregroundColor(.red).frame(width: 400))
}
}
struct ArcView: Shape {
let radius: CGFloat
func path(in rect: CGRect) -> Path {
return Path { path in
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: radius, startAngle: Angle(degrees: 0.0), endAngle: Angle(degrees: 360.0), clockwise: true)
}
}
}
struct LineView: Shape {
func path(in rect: CGRect) -> Path {
return Path { path in
path.move(to: CGPoint(x: rect.minX, y: rect.midY))
path.addLines([CGPoint(x: rect.minX, y: rect.midY), CGPoint(x: rect.maxX, y: rect.midY)])
}
}
}
result:

Can't use 'shape' as Type in SwiftUI

I'm trying to create a Set of cards. Each card has an object (shape) assigned, that is then shown with a different colour and appears in different numbers on each card. -> as a template I've created the struct SetCard
Already within the definition of the struct SetCard the var shape: Shape returns the error message:
Value of protocol type 'Shape' cannot conform to 'View'; only struct/enum/class types can conform to protocols"
import Foundation
import SwiftUI
var setCardSet: Array<SetCard> = []
var counter: Int = 0
func createSet() -> Array<SetCard> {
for object in 0..<3 {
var chosenShape = objectLibrary[object]
for color in 0..<3 {
var chosenColor = colors[color]
for numberOfShapes in 0..<3 {
counter += 1
setCardSet.append(SetCard(id: counter, shape: chosenShape, numberOfShapes: numberOfShapes, colorOfShapes: chosenColor))
}
}
}
return setCardSet
}
struct SetCard: Identifiable {
var id: Int
var shape: Shape
var numberOfShapes: Int
var colorOfShapes: Color
// var shadingOfShapes: Double
}
struct GameObject {
var name: String
var object: Shape
}
let objectLibrary: [Shape] = [Diamond(), Oval()]
let colors: [Color] = [Color.green, Color.red, Color.purple]
At a later step, the individual objects are shown and "stacked" on the same card:
import SwiftUI
struct ContentView: View {
let observed: Array<SetCard> = createSet()
var body: some View {
VStack (observed) { card in
CardView(card: card).onTapGesture {
withAnimation(.linear(duration: 0.75)) {
print(card.id)
}
}
.padding(2)
}
}
}
struct CardView: View {
var card: SetCard
var body: some View {
ZStack {
VStack{
ForEach(0 ..< card.numberOfShapes+1) {_ in
card.shape
}
}
}
}
}
let gradientStart = Color(red: 239.0 / 255, green: 120.0 / 255, blue: 221.0 / 255)
let gradientEnd = Color(red: 239.0 / 255, green: 172.0 / 255, blue: 120.0 / 255)
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
What am I doing wrong here?
Definition of shapes:
import SwiftUI
struct Diamond: Shape {
func path(in rect: CGRect) -> Path {
let height = min(rect.width, rect.height)/4
let length = min(rect.width, rect.height)/3
let center = CGPoint(x: rect.midX, y: rect.midY)
let top: CGPoint = CGPoint(x: center.x + length, y: center.y)
let left: CGPoint = CGPoint(x: center.x, y: center.y - height)
let bottom: CGPoint = CGPoint(x: center.x - length, y: center.y)
let right: CGPoint = CGPoint(x: center.x, y: center.y + height)
var p = Path()
p.move(to: top)
p.addLine(to: left)
p.addLine(to: bottom)
p.addLine(to: right)
p.addLine(to: top)
return p
}
}
struct Oval: Shape {
func path(in rect: CGRect) -> Path {
let height = min(rect.width, rect.height)/4
let length = min(rect.width, rect.height)/3
let center = CGPoint(x: rect.midX, y: rect.midY)
let centerLeft = CGPoint(x: center.x - length + (height/2), y: center.y)
let centerRight = CGPoint(x: center.x + length - (height/2), y: center.y)
let bottomRight = CGPoint(x: centerRight.x, y: center.y - height)
let topLeft = CGPoint(x: centerLeft.x, y: center.y + height)
var p = Path()
p.move(to: topLeft)
p.addArc(center: centerLeft, radius: height, startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 270), clockwise: false)
p.addLine(to: bottomRight)
p.addArc(center: centerRight, radius: height, startAngle: Angle(degrees: 270), endAngle: Angle(degrees: 90), clockwise: false)
p.addLine(to: topLeft)
return p
}
}
You need to use one type for shape in your model, like
struct SetCard: Identifiable {
var id: Int
var shape: AnyShape // << here !!
var numberOfShapes: Int
var colorOfShapes: Color
// var shadingOfShapes: Double
}
The AnyShape declaration and demo of usage can be observed in my different answer https://stackoverflow.com/a/62605936/12299030
And, of course, you have to update all other dependent parts to use it (I skipped that for simplicity).

Swift ui radar chart

Result final:
My result:
how can i do so to get the same result?
Code:
//
// RadarChartView.swift
// codeTutorial_radarChart
//
// Created by Christopher Guirguis on 4/4/20.
// Copyright © 2020 Christopher Guirguis. All rights reserved.
//
import SwiftUI
let dimensions = [
Ray(maxVal: 100, rayCase: .Base),
Ray(maxVal: 100, rayCase: .Content),
Ray(maxVal: 100, rayCase: .SpeedWeb),
Ray(maxVal: 100, rayCase: .SpeedMobile),
Ray(maxVal: 100, rayCase: .Social)
]
let data = [
DataPoint(base: 58.9, content: 20.3, speedweb: 91, speedmobile: 56.01, social: 55.6, color: .blue),
]
enum RayCase:String, CaseIterable {
case Base = "Base"
case Content = "Content"
case SpeedWeb = "SpeedWeb"
case SpeedMobile = "SpeedMobile"
case Social = "Social"
}
struct DataPoint:Identifiable {
var id = UUID()
var entrys:[RayEntry]
var color:Color
init(base:Double, content:Double, speedweb:Double, speedmobile:Double, social:Double, color:Color){
self.entrys = [
RayEntry(rayCase: .Base, value: base),
RayEntry(rayCase: .Content, value: content),
RayEntry(rayCase: .SpeedWeb, value: speedweb),
RayEntry(rayCase: .SpeedMobile, value: speedmobile),
RayEntry(rayCase: .Social, value: social)
]
self.color = color
}
}
struct Ray:Identifiable {
var id = UUID()
var name:String
var maxVal:Double
var rayCase:RayCase
init(maxVal:Double, rayCase:RayCase) {
self.rayCase = rayCase
self.name = rayCase.rawValue
self.maxVal = maxVal
}
}
struct RayEntry{
var rayCase:RayCase
var value:Double
}
func deg2rad(_ number: CGFloat) -> CGFloat {
return number * .pi / 180
}
func radAngle_fromFraction(numerator:Int, denominator: Int) -> CGFloat {
return deg2rad(360 * (CGFloat((numerator))/CGFloat(denominator)))
}
func degAngle_fromFraction(numerator:Int, denominator: Int) -> CGFloat {
return 360 * (CGFloat((numerator))/CGFloat(denominator))
}
struct RadarChartView: View {
var MainColor:Color
var SubtleColor:Color
var center:CGPoint
var labelWidth:CGFloat = 70
var width:CGFloat
var quantity_incrementalDividers:Int
var dimensions:[Ray]
var data:[DataPoint]
init(width: CGFloat, MainColor:Color, SubtleColor:Color, quantity_incrementalDividers:Int, dimensions:[Ray], data:[DataPoint]) {
self.width = width
self.center = CGPoint(x: width/2, y: width/2)
self.MainColor = MainColor
self.SubtleColor = SubtleColor
self.quantity_incrementalDividers = quantity_incrementalDividers
self.dimensions = dimensions
self.data = data
}
#State var showLabels = false
var body: some View {
ZStack{
//Main Spokes
Path { path in
for i in 0..<self.dimensions.count {
let angle = radAngle_fromFraction(numerator: i, denominator: self.dimensions.count)
let x = (self.width - (50 + self.labelWidth))/2 * cos(angle)
let y = (self.width - (50 + self.labelWidth))/2 * sin(angle)
path.move(to: center)
path.addLine(to: CGPoint(x: center.x + x, y: center.y + y))
}
}
.stroke(self.MainColor, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
//Labels
ForEach(0..<self.dimensions.count){i in
Text(self.dimensions[i].rayCase.rawValue)
.font(.system(size: 10))
.foregroundColor(self.SubtleColor)
.frame(width:self.labelWidth, height:10)
.rotationEffect(.degrees(
(degAngle_fromFraction(numerator: i, denominator: self.dimensions.count) > 90 && degAngle_fromFraction(numerator: i, denominator: self.dimensions.count) < 270) ? 180 : 0
))
.background(Color.clear)
.offset(x: (self.width - (50))/2)
.rotationEffect(.radians(
Double(radAngle_fromFraction(numerator: i, denominator: self.dimensions.count)
)))
}
//Outer Border
Path { path in
for i in 0..<self.dimensions.count + 1 {
let angle = radAngle_fromFraction(numerator: i, denominator: self.dimensions.count)
let x = (self.width - (50 + self.labelWidth))/2 * cos(angle)
let y = (self.width - (50 + self.labelWidth))/2 * sin(angle)
if i == 0 {
path.move(to: CGPoint(x: center.x + x, y: center.y + y))
} else {
path.addLine(to: CGPoint(x: center.x + x, y: center.y + y))
}
}
}
.stroke(self.MainColor, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
//Incremental Dividers
ForEach(0..<self.quantity_incrementalDividers){j in
Path { path in
for i in 0..<self.dimensions.count + 1 {
let angle = radAngle_fromFraction(numerator: i, denominator: self.dimensions.count)
let size = ((self.width - (50 + self.labelWidth))/2) * (CGFloat(j + 1)/CGFloat(self.quantity_incrementalDividers + 1))
let x = size * cos(angle)
let y = size * sin(angle)
print(size)
print((self.width - (50 + self.labelWidth)))
print("\(x) -- \(y)")
if i == 0 {
path.move(to: CGPoint(x: self.center.x + x, y: self.center.y + y))
} else {
path.addLine(to: CGPoint(x: self.center.x + x, y: self.center.y + y))
}
}
}
.stroke(self.SubtleColor, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round))
}
//Data Polygons
ForEach(0..<self.data.count){j -> AnyView in
//Create the path
let path = Path { path in
for i in 0..<self.dimensions.count + 1 {
let thisDimension = self.dimensions[i == self.dimensions.count ? 0 : i]
let maxVal = thisDimension.maxVal
let dataPointVal:Double = {
for DataPointRay in self.data[j].entrys {
if thisDimension.rayCase == DataPointRay.rayCase {
return DataPointRay.value
}
}
return 0
}()
let angle = radAngle_fromFraction(numerator: i == self.dimensions.count ? 0 : i, denominator: self.dimensions.count)
let size = ((self.width - (50 + self.labelWidth))/2) * (CGFloat(dataPointVal)/CGFloat(maxVal))
let x = size * cos(angle)
let y = size * sin(angle)
print(size)
print((self.width - (50 + self.labelWidth)))
print("\(x) -- \(y)")
if i == 0 {
path.move(to: CGPoint(x: self.center.x + x, y: self.center.y + y))
} else {
path.addLine(to: CGPoint(x: self.center.x + x, y: self.center.y + y))
}
}
}
//Stroke and Fill
return AnyView(
ZStack{
path
.stroke(self.data[j].color, style: StrokeStyle(lineWidth: 1.5, lineCap: .round, lineJoin: .round))
path
.foregroundColor(self.data[j].color).opacity(0.2)
}
)
}
}.frame(width:width, height:width)
}
}
struct RadarChartView_Previews: PreviewProvider {
static var previews: some View {
ZStack{
Color.black.edgesIgnoringSafeArea(.all)
VStack{
Text("Final evaluation")
.font(.system(size: 30, weight: .semibold))
RadarChartView(
width: 370,
MainColor: Color.init(white: 0.8),
SubtleColor: Color.init(white: 0.6),
quantity_incrementalDividers: 10,
dimensions: dimensions,
data: data
)
Spacer()
}.foregroundColor(.white)
}
}
}