From d869e4186f63d30a7d3e35f29f125cc00c8c0d9c Mon Sep 17 00:00:00 2001 From: Andras Samu Date: Fri, 22 May 2020 15:45:07 +0200 Subject: [PATCH 01/29] Replace the old with a more sleek and flexible code architecture --- Sources/SwiftUICharts/BarChart/BarChart.swift | 7 + .../SwiftUICharts/BarChart/BarChartCell.swift | 44 --- .../SwiftUICharts/BarChart/BarChartRow.swift | 50 --- .../SwiftUICharts/BarChart/BarChartView.swift | 148 -------- .../SwiftUICharts/BarChart/LabelView.swift | 46 --- .../SwiftUICharts/BarChart/TitleLabel.swift | 7 + .../Base/Chart/AnyChartType.swift | 19 + .../SwiftUICharts/Base/Chart/ChartType.swift | 11 + .../Base/Chart/ChartTypeConfiguration.swift | 5 + Sources/SwiftUICharts/Base/ChartView.swift | 15 + .../Base/Extensions/Color+Extension.swift | 21 ++ .../Base/Label/AnyChartLabel.swift | 19 + .../SwiftUICharts/Base/Label/ChartLabel.swift | 11 + .../Base/Label/ChartLabelConfiguration.swift | 7 + .../SwiftUICharts/Base/Style/ChartStyle.swift | 11 + .../Base/Style/ColorGradient.swift | 15 + Sources/SwiftUICharts/Base/Style/Colors.swift | 7 + .../Environment/Environment.swift | 30 ++ .../Environment/EnvironmentKeys.swift | 15 + Sources/SwiftUICharts/Helpers.swift | 278 -------------- .../LineChart/IndicatorPoint.swift | 28 -- Sources/SwiftUICharts/LineChart/Legend.swift | 99 ----- Sources/SwiftUICharts/LineChart/Line.swift | 107 ------ .../LineChart/LineChartView.swift | 148 -------- .../SwiftUICharts/LineChart/LineView.swift | 127 ------- .../LineChart/MagnifierRect.swift | 33 -- .../LineChart/MultiLineChartView.swift | 163 -------- .../LineChart/Path+QuadCurve.swift | 353 ------------------ .../SwiftUICharts/PieChart/PieChartCell.swift | 65 ---- .../SwiftUICharts/PieChart/PieChartRow.swift | 46 --- .../SwiftUICharts/PieChart/PieChartView.swift | 64 ---- 31 files changed, 200 insertions(+), 1799 deletions(-) create mode 100644 Sources/SwiftUICharts/BarChart/BarChart.swift delete mode 100644 Sources/SwiftUICharts/BarChart/BarChartCell.swift delete mode 100644 Sources/SwiftUICharts/BarChart/BarChartRow.swift delete mode 100644 Sources/SwiftUICharts/BarChart/BarChartView.swift delete mode 100644 Sources/SwiftUICharts/BarChart/LabelView.swift create mode 100644 Sources/SwiftUICharts/BarChart/TitleLabel.swift create mode 100644 Sources/SwiftUICharts/Base/Chart/AnyChartType.swift create mode 100644 Sources/SwiftUICharts/Base/Chart/ChartType.swift create mode 100644 Sources/SwiftUICharts/Base/Chart/ChartTypeConfiguration.swift create mode 100644 Sources/SwiftUICharts/Base/ChartView.swift create mode 100644 Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift create mode 100644 Sources/SwiftUICharts/Base/Label/AnyChartLabel.swift create mode 100644 Sources/SwiftUICharts/Base/Label/ChartLabel.swift create mode 100644 Sources/SwiftUICharts/Base/Label/ChartLabelConfiguration.swift create mode 100644 Sources/SwiftUICharts/Base/Style/ChartStyle.swift create mode 100644 Sources/SwiftUICharts/Base/Style/ColorGradient.swift create mode 100644 Sources/SwiftUICharts/Base/Style/Colors.swift create mode 100644 Sources/SwiftUICharts/Environment/Environment.swift create mode 100644 Sources/SwiftUICharts/Environment/EnvironmentKeys.swift delete mode 100644 Sources/SwiftUICharts/Helpers.swift delete mode 100644 Sources/SwiftUICharts/LineChart/IndicatorPoint.swift delete mode 100644 Sources/SwiftUICharts/LineChart/Legend.swift delete mode 100644 Sources/SwiftUICharts/LineChart/Line.swift delete mode 100644 Sources/SwiftUICharts/LineChart/LineChartView.swift delete mode 100644 Sources/SwiftUICharts/LineChart/LineView.swift delete mode 100644 Sources/SwiftUICharts/LineChart/MagnifierRect.swift delete mode 100644 Sources/SwiftUICharts/LineChart/MultiLineChartView.swift delete mode 100644 Sources/SwiftUICharts/LineChart/Path+QuadCurve.swift delete mode 100644 Sources/SwiftUICharts/PieChart/PieChartCell.swift delete mode 100644 Sources/SwiftUICharts/PieChart/PieChartRow.swift delete mode 100644 Sources/SwiftUICharts/PieChart/PieChartView.swift diff --git a/Sources/SwiftUICharts/BarChart/BarChart.swift b/Sources/SwiftUICharts/BarChart/BarChart.swift new file mode 100644 index 00000000..c7a271b6 --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/BarChart.swift @@ -0,0 +1,7 @@ +import SwiftUI + +public struct BarChart: ChartType { + public func makeChart(configuration: Self.Configuration) -> some View { + return Text("AAA") + } +} diff --git a/Sources/SwiftUICharts/BarChart/BarChartCell.swift b/Sources/SwiftUICharts/BarChart/BarChartCell.swift deleted file mode 100644 index a3500b7f..00000000 --- a/Sources/SwiftUICharts/BarChart/BarChartCell.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// ChartCell.swift -// ChartView -// -// Created by András Samu on 2019. 06. 12.. -// Copyright © 2019. András Samu. All rights reserved. -// - -import SwiftUI - -public struct BarChartCell : View { - var value: Double - var index: Int = 0 - var width: Float - var numberOfDataPoints: Int - var cellWidth: Double { - return Double(width)/(Double(numberOfDataPoints) * 1.5) - } - var accentColor: Color - var gradient: GradientColor? - - @State var scaleValue: Double = 0 - @Binding var touchLocation: CGFloat - public var body: some View { - ZStack { - RoundedRectangle(cornerRadius: 4) - .fill(LinearGradient(gradient: gradient?.getGradient() ?? GradientColor(start: accentColor, end: accentColor).getGradient(), startPoint: .bottom, endPoint: .top)) - } - .frame(width: CGFloat(self.cellWidth)) - .scaleEffect(CGSize(width: 1, height: self.scaleValue), anchor: .bottom) - .onAppear(){ - self.scaleValue = self.value - } - .animation(Animation.spring().delay(self.touchLocation < 0 ? Double(self.index) * 0.04 : 0)) - } -} - -#if DEBUG -struct ChartCell_Previews : PreviewProvider { - static var previews: some View { - BarChartCell(value: Double(0.75), width: 320, numberOfDataPoints: 12, accentColor: Colors.OrangeStart, gradient: nil, touchLocation: .constant(-1)) - } -} -#endif diff --git a/Sources/SwiftUICharts/BarChart/BarChartRow.swift b/Sources/SwiftUICharts/BarChart/BarChartRow.swift deleted file mode 100644 index 59b6a6d5..00000000 --- a/Sources/SwiftUICharts/BarChart/BarChartRow.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// ChartRow.swift -// ChartView -// -// Created by András Samu on 2019. 06. 12.. -// Copyright © 2019. András Samu. All rights reserved. -// - -import SwiftUI - -public struct BarChartRow : View { - var data: [Double] - var accentColor: Color - var gradient: GradientColor? - var maxValue: Double { - data.max() ?? 0 - } - @Binding var touchLocation: CGFloat - public var body: some View { - GeometryReader { geometry in - HStack(alignment: .bottom, spacing: (geometry.frame(in: .local).width-22)/CGFloat(self.data.count * 3)){ - ForEach(0.. CGFloat(i)/CGFloat(self.data.count) && self.touchLocation < CGFloat(i+1)/CGFloat(self.data.count) ? CGSize(width: 1.4, height: 1.1) : CGSize(width: 1, height: 1), anchor: .bottom) - .animation(.spring()) - - } - } - .padding([.top, .leading, .trailing], 10) - } - } - - func normalizedValue(index: Int) -> Double { - return Double(self.data[index])/Double(self.maxValue) - } -} - -#if DEBUG -struct ChartRow_Previews : PreviewProvider { - static var previews: some View { - BarChartRow(data: [8,23,54,32,12,37,7], accentColor: Colors.OrangeStart, touchLocation: .constant(-1)) - } -} -#endif diff --git a/Sources/SwiftUICharts/BarChart/BarChartView.swift b/Sources/SwiftUICharts/BarChart/BarChartView.swift deleted file mode 100644 index 541d8b5f..00000000 --- a/Sources/SwiftUICharts/BarChart/BarChartView.swift +++ /dev/null @@ -1,148 +0,0 @@ -// -// ChartView.swift -// ChartView -// -// Created by András Samu on 2019. 06. 12.. -// Copyright © 2019. András Samu. All rights reserved. -// - -import SwiftUI - -public struct BarChartView : View { - @Environment(\.colorScheme) var colorScheme: ColorScheme - private var data: ChartData - public var title: String - public var legend: String? - public var style: ChartStyle - public var darkModeStyle: ChartStyle - public var formSize:CGSize - public var dropShadow: Bool - public var cornerImage: Image - public var valueSpecifier:String - - @State private var touchLocation: CGFloat = -1.0 - @State private var showValue: Bool = false - @State private var showLabelValue: Bool = false - @State private var currentValue: Double = 0 { - didSet{ - if(oldValue != self.currentValue && self.showValue) { - HapticFeedback.playSelection() - } - } - } - var isFullWidth:Bool { - return self.formSize == ChartForm.large - } - public init(data:ChartData, title: String, legend: String? = nil, style: ChartStyle = Styles.barChartStyleOrangeLight, form: CGSize? = ChartForm.medium, dropShadow: Bool? = true, cornerImage:Image? = Image(systemName: "waveform.path.ecg"), valueSpecifier: String? = "%.1f"){ - self.data = data - self.title = title - self.legend = legend - self.style = style - self.darkModeStyle = style.darkModeStyle != nil ? style.darkModeStyle! : Styles.barChartStyleOrangeDark - self.formSize = form! - self.dropShadow = dropShadow! - self.cornerImage = cornerImage! - self.valueSpecifier = valueSpecifier! - } - - public var body: some View { - ZStack{ - Rectangle() - .fill(self.colorScheme == .dark ? self.darkModeStyle.backgroundColor : self.style.backgroundColor) - .cornerRadius(20) - .shadow(color: self.style.dropShadowColor, radius: self.dropShadow ? 8 : 0) - VStack(alignment: .leading){ - HStack{ - if(!showValue){ - Text(self.title) - .font(.headline) - .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor) - }else{ - Text("\(self.currentValue, specifier: self.valueSpecifier)") - .font(.headline) - .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor) - } - if(self.formSize == ChartForm.large && self.legend != nil && !showValue) { - Text(self.legend!) - .font(.callout) - .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.accentColor : self.style.accentColor) - .transition(.opacity) - .animation(.easeOut) - } - Spacer() - self.cornerImage - .imageScale(.large) - .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor) - }.padding() - BarChartRow(data: data.points.map{$0.1}, - accentColor: self.colorScheme == .dark ? self.darkModeStyle.accentColor : self.style.accentColor, - gradient: self.colorScheme == .dark ? self.darkModeStyle.gradientColor : self.style.gradientColor, - touchLocation: self.$touchLocation) - if self.legend != nil && self.formSize == ChartForm.medium && !self.showLabelValue{ - Text(self.legend!) - .font(.headline) - .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor) - .padding() - }else if (self.data.valuesGiven && self.getCurrentValue() != nil) { - LabelView(arrowOffset: self.getArrowOffset(touchLocation: self.touchLocation), - title: .constant(self.getCurrentValue()!.0)) - .offset(x: self.getLabelViewOffset(touchLocation: self.touchLocation), y: -6) - .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor) - } - - } - }.frame(minWidth:self.formSize.width, - maxWidth: self.isFullWidth ? .infinity : self.formSize.width, - minHeight:self.formSize.height, - maxHeight:self.formSize.height) - .gesture(DragGesture() - .onChanged({ value in - self.touchLocation = value.location.x/self.formSize.width - self.showValue = true - self.currentValue = self.getCurrentValue()?.1 ?? 0 - if(self.data.valuesGiven && self.formSize == ChartForm.medium) { - self.showLabelValue = true - } - }) - .onEnded({ value in - self.showValue = false - self.showLabelValue = false - self.touchLocation = -1 - }) - ) - .gesture(TapGesture() - ) - } - - func getArrowOffset(touchLocation:CGFloat) -> Binding { - let realLoc = (self.touchLocation * self.formSize.width) - 50 - if realLoc < 10 { - return .constant(realLoc - 10) - }else if realLoc > self.formSize.width-110 { - return .constant((self.formSize.width-110 - realLoc) * -1) - } else { - return .constant(0) - } - } - - func getLabelViewOffset(touchLocation:CGFloat) -> CGFloat { - return min(self.formSize.width-110,max(10,(self.touchLocation * self.formSize.width) - 50)) - } - - func getCurrentValue() -> (String,Double)? { - guard self.data.points.count > 0 else { return nil} - let index = max(0,min(self.data.points.count-1,Int(floor((self.touchLocation*self.formSize.width)/(self.formSize.width/CGFloat(self.data.points.count)))))) - return self.data.points[index] - } -} - -#if DEBUG -struct ChartView_Previews : PreviewProvider { - static var previews: some View { - BarChartView(data: TestData.values , - title: "Model 3 sales", - legend: "Quarterly", - valueSpecifier: "%.0f") - } -} -#endif diff --git a/Sources/SwiftUICharts/BarChart/LabelView.swift b/Sources/SwiftUICharts/BarChart/LabelView.swift deleted file mode 100644 index f17ae7be..00000000 --- a/Sources/SwiftUICharts/BarChart/LabelView.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// LabelView.swift -// BarChart -// -// Created by Samu András on 2020. 01. 08.. -// Copyright © 2020. Samu András. All rights reserved. -// - -import SwiftUI - -struct LabelView: View { - @Binding var arrowOffset: CGFloat - @Binding var title:String - var body: some View { - VStack{ - ArrowUp().fill(Color.white).frame(width: 20, height: 12, alignment: .center).shadow(color: Color.gray, radius: 8, x: 0, y: 0).offset(x: getArrowOffset(offset:self.arrowOffset), y: 12) - ZStack{ - RoundedRectangle(cornerRadius: 8).frame(width: 100, height: 32, alignment: .center).foregroundColor(Color.white).shadow(radius: 8) - Text(self.title).font(.caption).bold() - ArrowUp().fill(Color.white).frame(width: 20, height: 12, alignment: .center).zIndex(999).offset(x: getArrowOffset(offset:self.arrowOffset), y: -20) - - } - } - } - - func getArrowOffset(offset: CGFloat) -> CGFloat { - return max(-36,min(36, offset)) - } -} - -struct ArrowUp: Shape { - func path(in rect: CGRect) -> Path { - var path = Path() - path.move(to: CGPoint(x: 0, y: rect.height)) - path.addLine(to: CGPoint(x: rect.width/2, y: 0)) - path.addLine(to: CGPoint(x: rect.width, y: rect.height)) - path.closeSubpath() - return path - } -} - -struct LabelView_Previews: PreviewProvider { - static var previews: some View { - LabelView(arrowOffset: .constant(0), title: .constant("Tesla model 3")) - } -} diff --git a/Sources/SwiftUICharts/BarChart/TitleLabel.swift b/Sources/SwiftUICharts/BarChart/TitleLabel.swift new file mode 100644 index 00000000..c1962199 --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/TitleLabel.swift @@ -0,0 +1,7 @@ +import SwiftUI + +public struct TitleLabel: ChartLabel { + public func makeLabel(configuration: Self.Configuration) -> some View { + return Text("AAA") + } +} diff --git a/Sources/SwiftUICharts/Base/Chart/AnyChartType.swift b/Sources/SwiftUICharts/Base/Chart/AnyChartType.swift new file mode 100644 index 00000000..f5f97bd7 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Chart/AnyChartType.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct AnyChartType: ChartType { + private let chartMaker: (ChartType.Configuration) -> AnyView + + init(_ type: S) { + self.chartMaker = type.makeTypeErasedBody + } + + func makeChart(configuration: ChartType.Configuration) -> AnyView { + self.chartMaker(configuration) + } +} + +fileprivate extension ChartType { + func makeTypeErasedBody(configuration: ChartType.Configuration) -> AnyView { + AnyView(makeChart(configuration: configuration)) + } +} diff --git a/Sources/SwiftUICharts/Base/Chart/ChartType.swift b/Sources/SwiftUICharts/Base/Chart/ChartType.swift new file mode 100644 index 00000000..a432bae8 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Chart/ChartType.swift @@ -0,0 +1,11 @@ +import SwiftUI + +@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) +public protocol ChartType { + associatedtype Body : View + + func makeChart(configuration: Self.Configuration, style: Style) -> Self.Body + + typealias Configuration = ChartTypeConfiguration + typealias Style = ChartStyle +} diff --git a/Sources/SwiftUICharts/Base/Chart/ChartTypeConfiguration.swift b/Sources/SwiftUICharts/Base/Chart/ChartTypeConfiguration.swift new file mode 100644 index 00000000..1dbf8d80 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Chart/ChartTypeConfiguration.swift @@ -0,0 +1,5 @@ +import SwiftUI + +public struct ChartTypeConfiguration { + public let data: [CGFloat] +} diff --git a/Sources/SwiftUICharts/Base/ChartView.swift b/Sources/SwiftUICharts/Base/ChartView.swift new file mode 100644 index 00000000..98353e9c --- /dev/null +++ b/Sources/SwiftUICharts/Base/ChartView.swift @@ -0,0 +1,15 @@ +// ChartView.swift +// Created by Samu András on 2020. 05. 22.. + +import SwiftUI + +public struct ChartView: View { + @Environment(\.chartType) private var chartType + @Environment(\.chartStyle) private var chartStyle + @Environment(\.title) private var title + + public var body: some View { + Text("aa") + } +} + diff --git a/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift new file mode 100644 index 00000000..e8b24983 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift @@ -0,0 +1,21 @@ +import SwiftUI + +extension Color { + init(hexString: String) { + let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var int = UInt64() + Scanner(string: hex).scanHexInt64(&int) + let r, g, b: UInt64 + switch hex.count { + case 3: // RGB (12-bit) + (r, g, b) = ((int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + case 6: // RGB (24-bit) + (r, g, b) = (int >> 16, int >> 8 & 0xFF, int & 0xFF) + case 8: // ARGB (32-bit) + (r, g, b) = (int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + default: + (r, g, b) = (0, 0, 0) + } + self.init(red: Double(r) / 255, green: Double(g) / 255, blue: Double(b) / 255) + } +} diff --git a/Sources/SwiftUICharts/Base/Label/AnyChartLabel.swift b/Sources/SwiftUICharts/Base/Label/AnyChartLabel.swift new file mode 100644 index 00000000..7aaee0a0 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Label/AnyChartLabel.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct AnyChartLabel: ChartLabel { + private let labelMaker: (ChartLabel.Configuration) -> AnyView + + init(_ label: S) { + self.labelMaker = label.makeTypeErasedBody + } + + func makeLabel(configuration: ChartLabel.Configuration) -> AnyView { + self.labelMaker(configuration) + } +} + +fileprivate extension ChartLabel { + func makeTypeErasedBody(configuration: ChartLabel.Configuration) -> AnyView { + AnyView(makeLabel(configuration: configuration)) + } +} diff --git a/Sources/SwiftUICharts/Base/Label/ChartLabel.swift b/Sources/SwiftUICharts/Base/Label/ChartLabel.swift new file mode 100644 index 00000000..a3134e54 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Label/ChartLabel.swift @@ -0,0 +1,11 @@ +import SwiftUI + +@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) +public protocol ChartLabel { + + associatedtype Body : View + + func makeLabel(configuration: Self.Configuration) -> Self.Body + + typealias Configuration = ChartLabelConfiguration +} diff --git a/Sources/SwiftUICharts/Base/Label/ChartLabelConfiguration.swift b/Sources/SwiftUICharts/Base/Label/ChartLabelConfiguration.swift new file mode 100644 index 00000000..846126e8 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Label/ChartLabelConfiguration.swift @@ -0,0 +1,7 @@ +import SwiftUI + +public struct ChartLabelConfiguration { + public let font: Font + public let size: CGFloat + public let color: Color +} diff --git a/Sources/SwiftUICharts/Base/Style/ChartStyle.swift b/Sources/SwiftUICharts/Base/Style/ChartStyle.swift new file mode 100644 index 00000000..b7474f8c --- /dev/null +++ b/Sources/SwiftUICharts/Base/Style/ChartStyle.swift @@ -0,0 +1,11 @@ +import SwiftUI + +public struct ChartStyle { + public let backgroundColor: Color + public let foregroundColor: ColorGradient + + public init(backgroundColor: Color, foregroundColor: ColorGradient) { + self.backgroundColor = backgroundColor + self.foregroundColor = foregroundColor + } +} diff --git a/Sources/SwiftUICharts/Base/Style/ColorGradient.swift b/Sources/SwiftUICharts/Base/Style/ColorGradient.swift new file mode 100644 index 00000000..18a4ab20 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Style/ColorGradient.swift @@ -0,0 +1,15 @@ +import SwiftUI + +public struct ColorGradient { + public let startColor: Color + public let endColor: Color + + public init (_ startColor: Color, _ endColor: Color) { + self.startColor = startColor + self.endColor = endColor + } + + public var gradient: Gradient { + return Gradient(colors: [startColor, endColor]) + } +} diff --git a/Sources/SwiftUICharts/Base/Style/Colors.swift b/Sources/SwiftUICharts/Base/Style/Colors.swift new file mode 100644 index 00000000..5180d305 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Style/Colors.swift @@ -0,0 +1,7 @@ +import SwiftUI + +public enum ChartColors { + // Orange + static let orangeBright = Color(hexString: "#FF782C") + static let orangeDark = Color(hexString: "#EC2301") +} diff --git a/Sources/SwiftUICharts/Environment/Environment.swift b/Sources/SwiftUICharts/Environment/Environment.swift new file mode 100644 index 00000000..b90e9383 --- /dev/null +++ b/Sources/SwiftUICharts/Environment/Environment.swift @@ -0,0 +1,30 @@ +import SwiftUI + +extension EnvironmentValues { + var chartType: AnyChartType { + get { + return self[ChartTypeKey.self] + } + set { + self[ChartTypeKey.self] = newValue + } + } + + var chartStyle: ChartStyle { + get { + return self[ChartStyleKey.self] + } + set { + self[ChartStyleKey.self] = newValue + } + } + + var title: AnyChartLabel { + get { + return self[ChartLabelKey.self] + } + set { + self[ChartLabelKey.self] = newValue + } + } +} diff --git a/Sources/SwiftUICharts/Environment/EnvironmentKeys.swift b/Sources/SwiftUICharts/Environment/EnvironmentKeys.swift new file mode 100644 index 00000000..18983e5d --- /dev/null +++ b/Sources/SwiftUICharts/Environment/EnvironmentKeys.swift @@ -0,0 +1,15 @@ +import SwiftUI + +struct ChartTypeKey: EnvironmentKey { + static let defaultValue: AnyChartType = AnyChartType(BarChart()) +} + +struct ChartStyleKey: EnvironmentKey { + static let defaultValue: ChartStyle = ChartStyle(backgroundColor: .white, + foregroundColor: ColorGradient(ChartColors.orangeBright, + ChartColors.orangeDark)) +} + +struct ChartLabelKey: EnvironmentKey { + static let defaultValue: AnyChartLabel = AnyChartLabel(TitleLabel()) +} diff --git a/Sources/SwiftUICharts/Helpers.swift b/Sources/SwiftUICharts/Helpers.swift deleted file mode 100644 index a79bce54..00000000 --- a/Sources/SwiftUICharts/Helpers.swift +++ /dev/null @@ -1,278 +0,0 @@ -// -// File.swift -// -// -// Created by András Samu on 2019. 07. 19.. -// - -import Foundation -import SwiftUI - -public struct Colors { - public static let color1:Color = Color(hexString: "#E2FAE7") - public static let color1Accent:Color = Color(hexString: "#72BF82") - public static let color2:Color = Color(hexString: "#EEF1FF") - public static let color2Accent:Color = Color(hexString: "#4266E8") - public static let color3:Color = Color(hexString: "#FCECEA") - public static let color3Accent:Color = Color(hexString: "#E1614C") - public static let OrangeEnd:Color = Color(hexString: "#FF782C") - public static let OrangeStart:Color = Color(hexString: "#EC2301") - public static let LegendText:Color = Color(hexString: "#A7A6A8") - public static let LegendColor:Color = Color(hexString: "#E8E7EA") - public static let LegendDarkColor:Color = Color(hexString: "#545454") - public static let IndicatorKnob:Color = Color(hexString: "#FF57A6") - public static let GradientUpperBlue:Color = Color(hexString: "#C2E8FF") - public static let GradinetUpperBlue1:Color = Color(hexString: "#A8E1FF") - public static let GradientPurple:Color = Color(hexString: "#7B75FF") - public static let GradientNeonBlue:Color = Color(hexString: "#6FEAFF") - public static let GradientLowerBlue:Color = Color(hexString: "#F1F9FF") - public static let DarkPurple:Color = Color(hexString: "#1B205E") - public static let BorderBlue:Color = Color(hexString: "#4EBCFF") -} - -public struct GradientColor { - public let start: Color - public let end: Color - - public init(start: Color, end: Color) { - self.start = start - self.end = end - } - - public func getGradient() -> Gradient { - return Gradient(colors: [start, end]) - } -} - -public struct GradientColors { - public static let orange = GradientColor(start: Colors.OrangeStart, end: Colors.OrangeEnd) - public static let blue = GradientColor(start: Colors.GradientPurple, end: Colors.GradientNeonBlue) - public static let green = GradientColor(start: Color(hexString: "0BCDF7"), end: Color(hexString: "A2FEAE")) - public static let blu = GradientColor(start: Color(hexString: "0591FF"), end: Color(hexString: "29D9FE")) - public static let bluPurpl = GradientColor(start: Color(hexString: "4ABBFB"), end: Color(hexString: "8C00FF")) - public static let purple = GradientColor(start: Color(hexString: "741DF4"), end: Color(hexString: "C501B0")) - public static let prplPink = GradientColor(start: Color(hexString: "BC05AF"), end: Color(hexString: "FF1378")) - public static let prplNeon = GradientColor(start: Color(hexString: "FE019A"), end: Color(hexString: "FE0BF4")) - public static let orngPink = GradientColor(start: Color(hexString: "FF8E2D"), end: Color(hexString: "FF4E7A")) -} - -public struct Styles { - public static let lineChartStyleOne = ChartStyle( - backgroundColor: Color.white, - accentColor: Colors.OrangeStart, - secondGradientColor: Colors.OrangeEnd, - textColor: Color.black, - legendTextColor: Color.gray, - dropShadowColor: Color.gray) - - public static let barChartStyleOrangeLight = ChartStyle( - backgroundColor: Color.white, - accentColor: Colors.OrangeStart, - secondGradientColor: Colors.OrangeEnd, - textColor: Color.black, - legendTextColor: Color.gray, - dropShadowColor: Color.gray) - - public static let barChartStyleOrangeDark = ChartStyle( - backgroundColor: Color.black, - accentColor: Colors.OrangeStart, - secondGradientColor: Colors.OrangeEnd, - textColor: Color.white, - legendTextColor: Color.gray, - dropShadowColor: Color.gray) - - public static let barChartStyleNeonBlueLight = ChartStyle( - backgroundColor: Color.white, - accentColor: Colors.GradientNeonBlue, - secondGradientColor: Colors.GradientPurple, - textColor: Color.black, - legendTextColor: Color.gray, - dropShadowColor: Color.gray) - - public static let barChartStyleNeonBlueDark = ChartStyle( - backgroundColor: Color.black, - accentColor: Colors.GradientNeonBlue, - secondGradientColor: Colors.GradientPurple, - textColor: Color.white, - legendTextColor: Color.gray, - dropShadowColor: Color.gray) - - public static let barChartMidnightGreenDark = ChartStyle( - backgroundColor: Color(hexString: "#36534D"), //3B5147, 313D34 - accentColor: Color(hexString: "#FFD603"), - secondGradientColor: Color(hexString: "#FFCA04"), - textColor: Color.white, - legendTextColor: Color(hexString: "#D2E5E1"), - dropShadowColor: Color.gray) - - public static let barChartMidnightGreenLight = ChartStyle( - backgroundColor: Color.white, - accentColor: Color(hexString: "#84A094"), //84A094 , 698378 - secondGradientColor: Color(hexString: "#50675D"), - textColor: Color.black, - legendTextColor:Color.gray, - dropShadowColor: Color.gray) - - public static let pieChartStyleOne = ChartStyle( - backgroundColor: Color.white, - accentColor: Colors.OrangeEnd, - secondGradientColor: Colors.OrangeStart, - textColor: Color.black, - legendTextColor: Color.gray, - dropShadowColor: Color.gray) - - public static let lineViewDarkMode = ChartStyle( - backgroundColor: Color.black, - accentColor: Colors.OrangeStart, - secondGradientColor: Colors.OrangeEnd, - textColor: Color.white, - legendTextColor: Color.white, - dropShadowColor: Color.gray) -} - -public struct ChartForm { - #if os(watchOS) - public static let small = CGSize(width:120, height:90) - public static let medium = CGSize(width:120, height:160) - public static let large = CGSize(width:180, height:90) - public static let detail = CGSize(width:180, height:160) - #else - public static let small = CGSize(width:180, height:120) - public static let medium = CGSize(width:180, height:240) - public static let large = CGSize(width:360, height:120) - public static let detail = CGSize(width:180, height:120) - #endif - - -} - -public class ChartStyle { - public var backgroundColor: Color - public var accentColor: Color - public var gradientColor: GradientColor - public var textColor: Color - public var legendTextColor: Color - public var dropShadowColor: Color - public weak var darkModeStyle: ChartStyle? - - public init(backgroundColor: Color, accentColor: Color, secondGradientColor: Color, textColor: Color, legendTextColor: Color, dropShadowColor: Color){ - self.backgroundColor = backgroundColor - self.accentColor = accentColor - self.gradientColor = GradientColor(start: accentColor, end: secondGradientColor) - self.textColor = textColor - self.legendTextColor = legendTextColor - self.dropShadowColor = dropShadowColor - } - - public init(backgroundColor: Color, accentColor: Color, gradientColor: GradientColor, textColor: Color, legendTextColor: Color, dropShadowColor: Color){ - self.backgroundColor = backgroundColor - self.accentColor = accentColor - self.gradientColor = gradientColor - self.textColor = textColor - self.legendTextColor = legendTextColor - self.dropShadowColor = dropShadowColor - } - - public init(formSize: CGSize){ - self.backgroundColor = Color.white - self.accentColor = Colors.OrangeStart - self.gradientColor = GradientColors.orange - self.legendTextColor = Color.gray - self.textColor = Color.black - self.dropShadowColor = Color.gray - } -} - -public class ChartData: ObservableObject, Identifiable { - @Published var points: [(String,Double)] - var valuesGiven: Bool = false - var ID = UUID() - - public init(points:[N]) { - self.points = points.map{("", Double($0))} - } - public init(values:[(String,N)]){ - self.points = values.map{($0.0, Double($0.1))} - self.valuesGiven = true - } - public init(values:[(String,N)]){ - self.points = values.map{($0.0, Double($0.1))} - self.valuesGiven = true - } - public init(numberValues:[(N,N)]){ - self.points = numberValues.map{(String($0.0), Double($0.1))} - self.valuesGiven = true - } - public init(numberValues:[(N,N)]){ - self.points = numberValues.map{(String($0.0), Double($0.1))} - self.valuesGiven = true - } - - public func onlyPoints() -> [Double] { - return self.points.map{ $0.1 } - } -} - -public class MultiLineChartData: ChartData { - var gradient: GradientColor - - public init(points:[N], gradient: GradientColor) { - self.gradient = gradient - super.init(points: points) - } - - public init(points:[N], color: Color) { - self.gradient = GradientColor(start: color, end: color) - super.init(points: points) - } - - public func getGradient() -> GradientColor { - return self.gradient - } -} - -public class TestData{ - static public var data:ChartData = ChartData(points: [37,72,51,22,39,47,66,85,50]) - static public var values:ChartData = ChartData(values: [("2017 Q3",220), - ("2017 Q4",1550), - ("2018 Q1",8180), - ("2018 Q2",18440), - ("2018 Q3",55840), - ("2018 Q4",63150), ("2019 Q1",50900), ("2019 Q2",77550), ("2019 Q3",79600), ("2019 Q4",92550)]) - -} - -extension Color { - init(hexString: String) { - let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) - var int = UInt64() - Scanner(string: hex).scanHexInt64(&int) - let r, g, b: UInt64 - switch hex.count { - case 3: // RGB (12-bit) - (r, g, b) = ((int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) - case 6: // RGB (24-bit) - (r, g, b) = (int >> 16, int >> 8 & 0xFF, int & 0xFF) - case 8: // ARGB (32-bit) - (r, g, b) = (int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) - default: - (r, g, b) = (0, 0, 0) - } - self.init(red: Double(r) / 255, green: Double(g) / 255, blue: Double(b) / 255) - } -} - -class HapticFeedback { - #if os(watchOS) - //watchOS implementation - static func playSelection() -> Void { - WKInterfaceDevice.current().play(.click) - } - #else - //iOS implementation - let selectionFeedbackGenerator = UISelectionFeedbackGenerator() - static func playSelection() -> Void { - UISelectionFeedbackGenerator().selectionChanged() - } - #endif -} diff --git a/Sources/SwiftUICharts/LineChart/IndicatorPoint.swift b/Sources/SwiftUICharts/LineChart/IndicatorPoint.swift deleted file mode 100644 index 2e8667da..00000000 --- a/Sources/SwiftUICharts/LineChart/IndicatorPoint.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// IndicatorPoint.swift -// LineChart -// -// Created by András Samu on 2019. 09. 03.. -// Copyright © 2019. András Samu. All rights reserved. -// - -import SwiftUI - -struct IndicatorPoint: View { - var body: some View { - ZStack{ - Circle() - .fill(Colors.IndicatorKnob) - Circle() - .stroke(Color.white, style: StrokeStyle(lineWidth: 4)) - } - .frame(width: 14, height: 14) - .shadow(color: Colors.LegendColor, radius: 6, x: 0, y: 6) - } -} - -struct IndicatorPoint_Previews: PreviewProvider { - static var previews: some View { - IndicatorPoint() - } -} diff --git a/Sources/SwiftUICharts/LineChart/Legend.swift b/Sources/SwiftUICharts/LineChart/Legend.swift deleted file mode 100644 index b613cb06..00000000 --- a/Sources/SwiftUICharts/LineChart/Legend.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// Legend.swift -// LineChart -// -// Created by András Samu on 2019. 09. 02.. -// Copyright © 2019. András Samu. All rights reserved. -// - -import SwiftUI - -struct Legend: View { - @ObservedObject var data: ChartData - @Binding var frame: CGRect - @Binding var hideHorizontalLines: Bool - @Environment(\.colorScheme) var colorScheme: ColorScheme - let padding:CGFloat = 3 - - var stepWidth: CGFloat { - if data.points.count < 2 { - return 0 - } - return frame.size.width / CGFloat(data.points.count-1) - } - var stepHeight: CGFloat { - let points = self.data.onlyPoints() - if let min = points.min(), let max = points.max(), min != max { - if (min < 0){ - return (frame.size.height-padding) / CGFloat(max - min) - }else{ - return (frame.size.height-padding) / CGFloat(max + min) - } - } - return 0 - } - - var min: CGFloat { - let points = self.data.onlyPoints() - return CGFloat(points.min() ?? 0) - } - - var body: some View { - ZStack(alignment: .topLeading){ - ForEach((0...4), id: \.self) { height in - HStack(alignment: .center){ - Text("\(self.getYLegendSafe(height: height), specifier: "%.2f")").offset(x: 0, y: self.getYposition(height: height) ) - .foregroundColor(Colors.LegendText) - .font(.caption) - self.line(atHeight: self.getYLegendSafe(height: height), width: self.frame.width) - .stroke(self.colorScheme == .dark ? Colors.LegendDarkColor : Colors.LegendColor, style: StrokeStyle(lineWidth: 1.5, lineCap: .round, dash: [5,height == 0 ? 0 : 10])) - .opacity((self.hideHorizontalLines && height != 0) ? 0 : 1) - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) - .animation(.easeOut(duration: 0.2)) - .clipped() - } - - } - - } - } - - func getYLegendSafe(height:Int)->CGFloat{ - if let legend = getYLegend() { - return CGFloat(legend[height]) - } - return 0 - } - - func getYposition(height: Int)-> CGFloat { - if let legend = getYLegend() { - return (self.frame.height-((CGFloat(legend[height]) - min)*self.stepHeight))-(self.frame.height/2) - } - return 0 - - } - - func line(atHeight: CGFloat, width: CGFloat) -> Path { - var hLine = Path() - hLine.move(to: CGPoint(x:5, y: (atHeight-min)*stepHeight)) - hLine.addLine(to: CGPoint(x: width, y: (atHeight-min)*stepHeight)) - return hLine - } - - func getYLegend() -> [Double]? { - let points = self.data.onlyPoints() - guard let max = points.max() else { return nil } - guard let min = points.min() else { return nil } - let step = Double(max - min)/4 - return [min+step * 0, min+step * 1, min+step * 2, min+step * 3, min+step * 4] - } -} - -struct Legend_Previews: PreviewProvider { - static var previews: some View { - GeometryReader{ geometry in - Legend(data: ChartData(points: [0.2,0.4,1.4,4.5]), frame: .constant(geometry.frame(in: .local)), hideHorizontalLines: .constant(false)) - }.frame(width: 320, height: 200) - } -} diff --git a/Sources/SwiftUICharts/LineChart/Line.swift b/Sources/SwiftUICharts/LineChart/Line.swift deleted file mode 100644 index e85a8c3a..00000000 --- a/Sources/SwiftUICharts/LineChart/Line.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// Line.swift -// LineChart -// -// Created by András Samu on 2019. 08. 30.. -// Copyright © 2019. András Samu. All rights reserved. -// - -import SwiftUI - -public struct Line: View { - @ObservedObject var data: ChartData - @Binding var frame: CGRect - @Binding var touchLocation: CGPoint - @Binding var showIndicator: Bool - @Binding var minDataValue: Double? - @Binding var maxDataValue: Double? - @State private var showFull: Bool = false - @State var showBackground: Bool = true - var gradient: GradientColor = GradientColor(start: Colors.GradientPurple, end: Colors.GradientNeonBlue) - var index:Int = 0 - let padding:CGFloat = 30 - var curvedLines: Bool = true - var stepWidth: CGFloat { - if data.points.count < 2 { - return 0 - } - return frame.size.width / CGFloat(data.points.count-1) - } - var stepHeight: CGFloat { - var min: Double? - var max: Double? - let points = self.data.onlyPoints() - if minDataValue != nil && maxDataValue != nil { - min = minDataValue! - max = maxDataValue! - print(min,max) - }else if let minPoint = points.min(), let maxPoint = points.max(), minPoint != maxPoint { - min = minPoint - max = maxPoint - }else { - return 0 - } - if let min = min, let max = max, min != max { - if (min <= 0){ - return (frame.size.height-padding) / CGFloat(max - min) - }else{ - return (frame.size.height-padding) / CGFloat(max + min) - } - } - return 0 - } - var path: Path { - let points = self.data.onlyPoints() - return curvedLines ? Path.quadCurvedPathWithPoints(points: points, step: CGPoint(x: stepWidth, y: stepHeight), globalOffset: minDataValue) : Path.linePathWithPoints(points: points, step: CGPoint(x: stepWidth, y: stepHeight)) - } - var closedPath: Path { - let points = self.data.onlyPoints() - return curvedLines ? Path.quadClosedCurvedPathWithPoints(points: points, step: CGPoint(x: stepWidth, y: stepHeight), globalOffset: minDataValue) : Path.closedLinePathWithPoints(points: points, step: CGPoint(x: stepWidth, y: stepHeight)) - } - - public var body: some View { - ZStack { - if(self.showFull && self.showBackground){ - self.closedPath - .fill(LinearGradient(gradient: Gradient(colors: [Colors.GradientUpperBlue, .white]), startPoint: .bottom, endPoint: .top)) - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) - .transition(.opacity) - .animation(.easeIn(duration: 1.6)) - } - self.path - .trim(from: 0, to: self.showFull ? 1:0) - .stroke(LinearGradient(gradient: gradient.getGradient(), startPoint: .leading, endPoint: .trailing) ,style: StrokeStyle(lineWidth: 3, lineJoin: .round)) - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) - .animation(Animation.easeOut(duration: 1.2).delay(Double(self.index)*0.4)) - .onAppear { - self.showFull = true - } - .onDisappear { - self.showFull = false - } - .drawingGroup() - if(self.showIndicator) { - IndicatorPoint() - .position(self.getClosestPointOnPath(touchLocation: self.touchLocation)) - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) - } - } - } - - func getClosestPointOnPath(touchLocation: CGPoint) -> CGPoint { - let closest = self.path.point(to: touchLocation.x) - return closest - } - -} - -struct Line_Previews: PreviewProvider { - static var previews: some View { - GeometryReader{ geometry in - Line(data: ChartData(points: [12,-230,10,54]), frame: .constant(geometry.frame(in: .local)), touchLocation: .constant(CGPoint(x: 100, y: 12)), showIndicator: .constant(true), minDataValue: .constant(nil), maxDataValue: .constant(nil)) - }.frame(width: 320, height: 160) - } -} diff --git a/Sources/SwiftUICharts/LineChart/LineChartView.swift b/Sources/SwiftUICharts/LineChart/LineChartView.swift deleted file mode 100644 index 2726f083..00000000 --- a/Sources/SwiftUICharts/LineChart/LineChartView.swift +++ /dev/null @@ -1,148 +0,0 @@ -// -// LineCard.swift -// LineChart -// -// Created by András Samu on 2019. 08. 31.. -// Copyright © 2019. András Samu. All rights reserved. -// - -import SwiftUI - -public struct LineChartView: View { - @Environment(\.colorScheme) var colorScheme: ColorScheme - @ObservedObject var data:ChartData - public var title: String - public var legend: String? - public var style: ChartStyle - public var darkModeStyle: ChartStyle - - public var formSize:CGSize - public var dropShadow: Bool - public var valueSpecifier:String - - @State private var touchLocation:CGPoint = .zero - @State private var showIndicatorDot: Bool = false - @State private var currentValue: Double = 2 { - didSet{ - if (oldValue != self.currentValue && showIndicatorDot) { - HapticFeedback.playSelection() - } - - } - } - let frame = CGSize(width: 180, height: 120) - private var rateValue: Int? - - public init(data: [Double], - title: String, - legend: String? = nil, - style: ChartStyle = Styles.lineChartStyleOne, - form: CGSize? = ChartForm.medium, - rateValue: Int? = 14, - dropShadow: Bool? = true, - valueSpecifier: String? = "%.1f") { - - self.data = ChartData(points: data) - self.title = title - self.legend = legend - self.style = style - self.darkModeStyle = style.darkModeStyle != nil ? style.darkModeStyle! : Styles.lineViewDarkMode - self.formSize = form! - self.dropShadow = dropShadow! - self.valueSpecifier = valueSpecifier! - self.rateValue = rateValue - } - - public var body: some View { - ZStack(alignment: .center){ - RoundedRectangle(cornerRadius: 20) - .fill(self.colorScheme == .dark ? self.darkModeStyle.backgroundColor : self.style.backgroundColor) - .frame(width: frame.width, height: 240, alignment: .center) - .shadow(color: self.style.dropShadowColor, radius: self.dropShadow ? 8 : 0) - VStack(alignment: .leading){ - if(!self.showIndicatorDot){ - VStack(alignment: .leading, spacing: 8){ - Text(self.title) - .font(.title) - .bold() - .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor) - if (self.legend != nil){ - Text(self.legend!) - .font(.callout) - .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor :self.style.legendTextColor) - } - HStack { - - if (self.rateValue ?? 0 != 0) - { - if (self.rateValue ?? 0 >= 0){ - Image(systemName: "arrow.up") - }else{ - Image(systemName: "arrow.down") - } - Text("\(self.rateValue!)%") - } - } - } - .transition(.opacity) - .animation(.easeIn(duration: 0.1)) - .padding([.leading, .top]) - }else{ - HStack{ - Spacer() - Text("\(self.currentValue, specifier: self.valueSpecifier)") - .font(.system(size: 41, weight: .bold, design: .default)) - .offset(x: 0, y: 30) - Spacer() - } - .transition(.scale) - } - Spacer() - GeometryReader{ geometry in - Line(data: self.data, - frame: .constant(geometry.frame(in: .local)), - touchLocation: self.$touchLocation, - showIndicator: self.$showIndicatorDot, - minDataValue: .constant(nil), - maxDataValue: .constant(nil) - ) - } - .frame(width: frame.width, height: frame.height + 30) - .clipShape(RoundedRectangle(cornerRadius: 20)) - .offset(x: 0, y: 0) - }.frame(width: self.formSize.width, height: self.formSize.height) - } - .gesture(DragGesture() - .onChanged({ value in - self.touchLocation = value.location - self.showIndicatorDot = true - self.getClosestDataPoint(toPoint: value.location, width:self.frame.width, height: self.frame.height) - }) - .onEnded({ value in - self.showIndicatorDot = false - }) - ) - } - - @discardableResult func getClosestDataPoint(toPoint: CGPoint, width:CGFloat, height: CGFloat) -> CGPoint { - let points = self.data.onlyPoints() - let stepWidth: CGFloat = width / CGFloat(points.count-1) - let stepHeight: CGFloat = height / CGFloat(points.max()! + points.min()!) - - let index:Int = Int(round((toPoint.x)/stepWidth)) - if (index >= 0 && index < points.count){ - self.currentValue = points[index] - return CGPoint(x: CGFloat(index)*stepWidth, y: CGFloat(points[index])*stepHeight) - } - return .zero - } -} - -struct WidgetView_Previews: PreviewProvider { - static var previews: some View { - Group { - LineChartView(data: [8,23,54,32,12,37,7,23,43], title: "Line chart", legend: "Basic") - .environment(\.colorScheme, .light) - } - } -} diff --git a/Sources/SwiftUICharts/LineChart/LineView.swift b/Sources/SwiftUICharts/LineChart/LineView.swift deleted file mode 100644 index c4313aaf..00000000 --- a/Sources/SwiftUICharts/LineChart/LineView.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// LineView.swift -// LineChart -// -// Created by András Samu on 2019. 09. 02.. -// Copyright © 2019. András Samu. All rights reserved. -// - -import SwiftUI - -public struct LineView: View { - @ObservedObject var data: ChartData - public var title: String? - public var legend: String? - public var style: ChartStyle - public var darkModeStyle: ChartStyle - public var valueSpecifier:String - - @Environment(\.colorScheme) var colorScheme: ColorScheme - @State private var showLegend = false - @State private var dragLocation:CGPoint = .zero - @State private var indicatorLocation:CGPoint = .zero - @State private var closestPoint: CGPoint = .zero - @State private var opacity:Double = 0 - @State private var currentDataNumber: Double = 0 - @State private var hideHorizontalLines: Bool = false - - public init(data: [Double], - title: String? = nil, - legend: String? = nil, - style: ChartStyle = Styles.lineChartStyleOne, - valueSpecifier: String? = "%.1f") { - - self.data = ChartData(points: data) - self.title = title - self.legend = legend - self.style = style - self.valueSpecifier = valueSpecifier! - self.darkModeStyle = style.darkModeStyle != nil ? style.darkModeStyle! : Styles.lineViewDarkMode - } - - public var body: some View { - GeometryReader{ geometry in - VStack(alignment: .leading, spacing: 8) { - Group{ - if (self.title != nil){ - Text(self.title!) - .font(.title) - .bold().foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor) - } - if (self.legend != nil){ - Text(self.legend!) - .font(.callout) - .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor) - } - }.offset(x: 0, y: 20) - ZStack{ - GeometryReader{ reader in - Rectangle() - .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.backgroundColor : self.style.backgroundColor) - if(self.showLegend){ - Legend(data: self.data, - frame: .constant(reader.frame(in: .local)), hideHorizontalLines: self.$hideHorizontalLines) - .transition(.opacity) - .animation(Animation.easeOut(duration: 1).delay(1)) - } - Line(data: self.data, - frame: .constant(CGRect(x: 0, y: 0, width: reader.frame(in: .local).width - 30, height: reader.frame(in: .local).height)), - touchLocation: self.$indicatorLocation, - showIndicator: self.$hideHorizontalLines, - minDataValue: .constant(nil), - maxDataValue: .constant(nil), - showBackground: false, - gradient: self.style.gradientColor - ) - .offset(x: 30, y: 0) - .onAppear(){ - self.showLegend = true - } - .onDisappear(){ - self.showLegend = false - } - } - .frame(width: geometry.frame(in: .local).size.width, height: 240) - .offset(x: 0, y: 40 ) - MagnifierRect(currentNumber: self.$currentDataNumber, valueSpecifier: self.valueSpecifier) - .opacity(self.opacity) - .offset(x: self.dragLocation.x - geometry.frame(in: .local).size.width/2, y: 36) - } - .frame(width: geometry.frame(in: .local).size.width, height: 240) - .gesture(DragGesture() - .onChanged({ value in - self.dragLocation = value.location - self.indicatorLocation = CGPoint(x: max(value.location.x-30,0), y: 32) - self.opacity = 1 - self.closestPoint = self.getClosestDataPoint(toPoint: value.location, width: geometry.frame(in: .local).size.width-30, height: 240) - self.hideHorizontalLines = true - }) - .onEnded({ value in - self.opacity = 0 - self.hideHorizontalLines = false - }) - ) - } - } - } - - func getClosestDataPoint(toPoint: CGPoint, width:CGFloat, height: CGFloat) -> CGPoint { - let points = self.data.onlyPoints() - let stepWidth: CGFloat = width / CGFloat(points.count-1) - let stepHeight: CGFloat = height / CGFloat(points.max()! + points.min()!) - - let index:Int = Int(floor((toPoint.x-15)/stepWidth)) - if (index >= 0 && index < points.count){ - self.currentDataNumber = points[index] - return CGPoint(x: CGFloat(index)*stepWidth, y: CGFloat(points[index])*stepHeight) - } - return .zero - } -} - -struct LineView_Previews: PreviewProvider { - static var previews: some View { - LineView(data: [8,23,54,32,12,37,7,23,43], title: "Full chart", style: Styles.lineChartStyleOne) - } -} - diff --git a/Sources/SwiftUICharts/LineChart/MagnifierRect.swift b/Sources/SwiftUICharts/LineChart/MagnifierRect.swift deleted file mode 100644 index 4d3fd869..00000000 --- a/Sources/SwiftUICharts/LineChart/MagnifierRect.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// MagnifierRect.swift -// -// -// Created by Samu András on 2020. 03. 04.. -// - -import SwiftUI - -public struct MagnifierRect: View { - @Binding var currentNumber: Double - var valueSpecifier:String - @Environment(\.colorScheme) var colorScheme: ColorScheme - public var body: some View { - ZStack{ - Text("\(self.currentNumber, specifier: valueSpecifier)") - .font(.system(size: 18, weight: .bold)) - .offset(x: 0, y:-110) - .foregroundColor(self.colorScheme == .dark ? Color.white : Color.black) - if (self.colorScheme == .dark ){ - RoundedRectangle(cornerRadius: 16) - .stroke(Color.white, lineWidth: self.colorScheme == .dark ? 2 : 0) - .frame(width: 60, height: 260) - }else{ - RoundedRectangle(cornerRadius: 16) - .frame(width: 60, height: 280) - .foregroundColor(Color.white) - .shadow(color: Colors.LegendText, radius: 12, x: 0, y: 6 ) - .blendMode(.multiply) - } - } - } -} diff --git a/Sources/SwiftUICharts/LineChart/MultiLineChartView.swift b/Sources/SwiftUICharts/LineChart/MultiLineChartView.swift deleted file mode 100644 index 720da66d..00000000 --- a/Sources/SwiftUICharts/LineChart/MultiLineChartView.swift +++ /dev/null @@ -1,163 +0,0 @@ -// -// File.swift -// -// -// Created by Samu András on 2020. 02. 19.. -// - -import SwiftUI - -public struct MultiLineChartView: View { - @Environment(\.colorScheme) var colorScheme: ColorScheme - var data:[MultiLineChartData] - public var title: String - public var legend: String? - public var style: ChartStyle - public var darkModeStyle: ChartStyle - public var formSize:CGSize - public var dropShadow: Bool - public var valueSpecifier:String - - @State private var touchLocation:CGPoint = .zero - @State private var showIndicatorDot: Bool = false - @State private var currentValue: Double = 2 { - didSet{ - if (oldValue != self.currentValue && showIndicatorDot) { - HapticFeedback.playSelection() - } - - } - } - - var globalMin:Double { - if let min = data.flatMap({$0.onlyPoints()}).min() { - return min - } - return 0 - } - - var globalMax:Double { - if let max = data.flatMap({$0.onlyPoints()}).max() { - return max - } - return 0 - } - - let frame = CGSize(width: 180, height: 120) - private var rateValue: Int - - public init(data: [([Double], GradientColor)], - title: String, - legend: String? = nil, - style: ChartStyle = Styles.lineChartStyleOne, - form: CGSize? = ChartForm.medium, - rateValue: Int? = 14, - dropShadow: Bool? = true, - valueSpecifier: String? = "%.1f") { - - self.data = data.map({ MultiLineChartData(points: $0.0, gradient: $0.1)}) - self.title = title - self.legend = legend - self.style = style - self.darkModeStyle = style.darkModeStyle != nil ? style.darkModeStyle! : Styles.lineViewDarkMode - self.formSize = form! - self.rateValue = rateValue! - self.dropShadow = dropShadow! - self.valueSpecifier = valueSpecifier! - } - - public var body: some View { - ZStack(alignment: .center){ - RoundedRectangle(cornerRadius: 20) - .fill(self.colorScheme == .dark ? self.darkModeStyle.backgroundColor : self.style.backgroundColor) - .frame(width: frame.width, height: 240, alignment: .center) - .shadow(radius: self.dropShadow ? 8 : 0) - VStack(alignment: .leading){ - if(!self.showIndicatorDot){ - VStack(alignment: .leading, spacing: 8){ - Text(self.title) - .font(.title) - .bold() - .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor) - if (self.legend != nil){ - Text(self.legend!) - .font(.callout) - .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor) - } - HStack { - if (self.rateValue >= 0){ - Image(systemName: "arrow.up") - }else{ - Image(systemName: "arrow.down") - } - Text("\(self.rateValue)%") - } - } - .transition(.opacity) - .animation(.easeIn(duration: 0.1)) - .padding([.leading, .top]) - }else{ - HStack{ - Spacer() - Text("\(self.currentValue, specifier: self.valueSpecifier)") - .font(.system(size: 41, weight: .bold, design: .default)) - .offset(x: 0, y: 30) - Spacer() - } - .transition(.scale) - } - Spacer() - GeometryReader{ geometry in - ZStack{ - ForEach(0.. CGPoint { -// let points = self.data.onlyPoints() -// let stepWidth: CGFloat = width / CGFloat(points.count-1) -// let stepHeight: CGFloat = height / CGFloat(points.max()! + points.min()!) -// -// let index:Int = Int(round((toPoint.x)/stepWidth)) -// if (index >= 0 && index < points.count){ -// self.currentValue = points[index] -// return CGPoint(x: CGFloat(index)*stepWidth, y: CGFloat(points[index])*stepHeight) -// } -// return .zero -// } -} - -struct MultiWidgetView_Previews: PreviewProvider { - static var previews: some View { - Group { - MultiLineChartView(data: [([8,23,54,32,12,37,7,23,43], GradientColors.orange)], title: "Line chart", legend: "Basic") - .environment(\.colorScheme, .light) - } - } -} diff --git a/Sources/SwiftUICharts/LineChart/Path+QuadCurve.swift b/Sources/SwiftUICharts/LineChart/Path+QuadCurve.swift deleted file mode 100644 index 83cf114b..00000000 --- a/Sources/SwiftUICharts/LineChart/Path+QuadCurve.swift +++ /dev/null @@ -1,353 +0,0 @@ -// -// File.swift -// -// -// Created by xspyhack on 2020/1/21. -// - -import SwiftUI - -extension Path { - func trimmedPath(for percent: CGFloat) -> Path { - // percent difference between points - let boundsDistance: CGFloat = 0.001 - let completion: CGFloat = 1 - boundsDistance - - let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent) - - let start = pct > completion ? completion : pct - boundsDistance - let end = pct > completion ? 1 : pct + boundsDistance - return trimmedPath(from: start, to: end) - } - - func point(for percent: CGFloat) -> CGPoint { - let path = trimmedPath(for: percent) - return CGPoint(x: path.boundingRect.midX, y: path.boundingRect.midY) - } - - func point(to maxX: CGFloat) -> CGPoint { - let total = length - let sub = length(to: maxX) - let percent = sub / total - return point(for: percent) - } - - var length: CGFloat { - var ret: CGFloat = 0.0 - var start: CGPoint? - var point = CGPoint.zero - - forEach { ele in - switch ele { - case .move(let to): - if start == nil { - start = to - } - point = to - case .line(let to): - ret += point.line(to: to) - point = to - case .quadCurve(let to, let control): - ret += point.quadCurve(to: to, control: control) - point = to - case .curve(let to, let control1, let control2): - ret += point.curve(to: to, control1: control1, control2: control2) - point = to - case .closeSubpath: - if let to = start { - ret += point.line(to: to) - point = to - } - start = nil - } - } - return ret - } - - func length(to maxX: CGFloat) -> CGFloat { - var ret: CGFloat = 0.0 - var start: CGPoint? - var point = CGPoint.zero - var finished = false - - forEach { ele in - if finished { - return - } - switch ele { - case .move(let to): - if to.x > maxX { - finished = true - return - } - if start == nil { - start = to - } - point = to - case .line(let to): - if to.x > maxX { - finished = true - ret += point.line(to: to, x: maxX) - return - } - ret += point.line(to: to) - point = to - case .quadCurve(let to, let control): - if to.x > maxX { - finished = true - ret += point.quadCurve(to: to, control: control, x: maxX) - return - } - ret += point.quadCurve(to: to, control: control) - point = to - case .curve(let to, let control1, let control2): - if to.x > maxX { - finished = true - ret += point.curve(to: to, control1: control1, control2: control2, x: maxX) - return - } - ret += point.curve(to: to, control1: control1, control2: control2) - point = to - case .closeSubpath: - fatalError("Can't include closeSubpath") - } - } - return ret - } - - static func quadCurvedPathWithPoints(points:[Double], step:CGPoint, globalOffset: Double? = nil) -> Path { - var path = Path() - if (points.count < 2){ - return path - } - let offset = globalOffset ?? points.min()! -// guard let offset = points.min() else { return path } - var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y) - path.move(to: p1) - for pointIndex in 1.. Path { - var path = Path() - if (points.count < 2){ - return path - } - let offset = globalOffset ?? points.min()! - -// guard let offset = points.min() else { return path } - path.move(to: .zero) - var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y) - path.addLine(to: p1) - for pointIndex in 1.. Path { - var path = Path() - if (points.count < 2){ - return path - } - guard let offset = points.min() else { return path } - let p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y) - path.move(to: p1) - for pointIndex in 1.. Path { - var path = Path() - if (points.count < 2){ - return path - } - guard let offset = points.min() else { return path } - var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y) - path.move(to: p1) - for pointIndex in 1.. CGPoint { - let a = (to.y - self.y) / (to.x - self.x) - let y = self.y + (x - self.x) * a - return CGPoint(x: x, y: y) - } - - func line(to: CGPoint) -> CGFloat { - dist(to: to) - } - - func line(to: CGPoint, x: CGFloat) -> CGFloat { - dist(to: point(to: to, x: x)) - } - - func quadCurve(to: CGPoint, control: CGPoint) -> CGFloat { - var dist: CGFloat = 0 - let steps: CGFloat = 100 - - for i in 0.. CGFloat { - var dist: CGFloat = 0 - let steps: CGFloat = 100 - - for i in 0..= x { - return dist - } else if b.x > x { - dist += a.line(to: b, x: x) - return dist - } else if b.x == x { - dist += a.line(to: b) - return dist - } - - dist += a.line(to: b) - } - return dist - } - - func point(to: CGPoint, t: CGFloat, control: CGPoint) -> CGPoint { - let x = CGPoint.value(x: self.x, y: to.x, t: t, c: control.x) - let y = CGPoint.value(x: self.y, y: to.y, t: t, c: control.y) - - return CGPoint(x: x, y: y) - } - - func curve(to: CGPoint, control1: CGPoint, control2: CGPoint) -> CGFloat { - var dist: CGFloat = 0 - let steps: CGFloat = 100 - - for i in 0.. CGFloat { - var dist: CGFloat = 0 - let steps: CGFloat = 100 - - for i in 0..= x { - return dist - } else if b.x > x { - dist += a.line(to: b, x: x) - return dist - } else if b.x == x { - dist += a.line(to: b) - return dist - } - - dist += a.line(to: b) - } - - return dist - } - - func point(to: CGPoint, t: CGFloat, control1: CGPoint, control2: CGPoint) -> CGPoint { - let x = CGPoint.value(x: self.x, y: to.x, t: t, c1: control1.x, c2: control2.x) - let y = CGPoint.value(x: self.y, y: to.y, t: t, c1: control1.y, c2: control2.x) - - return CGPoint(x: x, y: y) - } - - static func value(x: CGFloat, y: CGFloat, t: CGFloat, c: CGFloat) -> CGFloat { - var value: CGFloat = 0.0 - // (1-t)^2 * p0 + 2 * (1-t) * t * c1 + t^2 * p1 - value += pow(1-t, 2) * x - value += 2 * (1-t) * t * c - value += pow(t, 2) * y - return value - } - - static func value(x: CGFloat, y: CGFloat, t: CGFloat, c1: CGFloat, c2: CGFloat) -> CGFloat { - var value: CGFloat = 0.0 - // (1-t)^3 * p0 + 3 * (1-t)^2 * t * c1 + 3 * (1-t) * t^2 * c2 + t^3 * p1 - value += pow(1-t, 3) * x - value += 3 * pow(1-t, 2) * t * c1 - value += 3 * (1-t) * pow(t, 2) * c2 - value += pow(t, 3) * y - return value - } - - static func getMidPoint(point1: CGPoint, point2: CGPoint) -> CGPoint { - return CGPoint( - x: point1.x + (point2.x - point1.x) / 2, - y: point1.y + (point2.y - point1.y) / 2 - ) - } - - func dist(to: CGPoint) -> CGFloat { - return sqrt((pow(self.x - to.x, 2) + pow(self.y - to.y, 2))) - } - - static func midPointForPoints(p1:CGPoint, p2:CGPoint) -> CGPoint { - return CGPoint(x:(p1.x + p2.x) / 2,y: (p1.y + p2.y) / 2) - } - - static func controlPointForPoints(p1:CGPoint, p2:CGPoint) -> CGPoint { - var controlPoint = CGPoint.midPointForPoints(p1:p1, p2:p2) - let diffY = abs(p2.y - controlPoint.y) - - if (p1.y < p2.y){ - controlPoint.y += diffY - } else if (p1.y > p2.y) { - controlPoint.y -= diffY - } - return controlPoint - } -} - diff --git a/Sources/SwiftUICharts/PieChart/PieChartCell.swift b/Sources/SwiftUICharts/PieChart/PieChartCell.swift deleted file mode 100644 index f511165e..00000000 --- a/Sources/SwiftUICharts/PieChart/PieChartCell.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// PieChartCell.swift -// ChartView -// -// Created by András Samu on 2019. 06. 12.. -// Copyright © 2019. András Samu. All rights reserved. -// - -import SwiftUI - -struct PieSlice: Identifiable { - var id = UUID() - var startDeg: Double - var endDeg: Double - var value: Double - var normalizedValue: Double -} - -public struct PieChartCell : View { - @State private var show:Bool = false - var rect: CGRect - var radius: CGFloat { - return min(rect.width, rect.height)/2 - } - var startDeg: Double - var endDeg: Double - var path: Path { - var path = Path() - path.addArc(center:rect.mid , radius:self.radius, startAngle: Angle(degrees: self.startDeg), endAngle: Angle(degrees: self.endDeg), clockwise: false) - path.addLine(to: rect.mid) - path.closeSubpath() - return path - } - var index: Int - var backgroundColor:Color - var accentColor:Color - public var body: some View { - path - .fill() - .foregroundColor(self.accentColor) - .overlay(path.stroke(self.backgroundColor, lineWidth: 2)) - .scaleEffect(self.show ? 1 : 0) - .animation(Animation.spring().delay(Double(self.index) * 0.04)) - .onAppear(){ - self.show = true - } - } -} - -extension CGRect { - var mid: CGPoint { - return CGPoint(x:self.midX, y: self.midY) - } -} - -#if DEBUG -struct PieChartCell_Previews : PreviewProvider { - static var previews: some View { - GeometryReader { geometry in - PieChartCell(rect: geometry.frame(in: .local),startDeg: 0.0,endDeg: 90.0, index: 0, backgroundColor: Color(red: 252.0/255.0, green: 236.0/255.0, blue: 234.0/255.0), accentColor: Color(red: 225.0/255.0, green: 97.0/255.0, blue: 76.0/255.0)) - }.frame(width:100, height:100) - - } -} -#endif diff --git a/Sources/SwiftUICharts/PieChart/PieChartRow.swift b/Sources/SwiftUICharts/PieChart/PieChartRow.swift deleted file mode 100644 index dd690d02..00000000 --- a/Sources/SwiftUICharts/PieChart/PieChartRow.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// PieChartRow.swift -// ChartView -// -// Created by András Samu on 2019. 06. 12.. -// Copyright © 2019. András Samu. All rights reserved. -// - -import SwiftUI - -public struct PieChartRow : View { - var data: [Double] - var backgroundColor: Color - var accentColor: Color - var slices: [PieSlice] { - var tempSlices:[PieSlice] = [] - var lastEndDeg:Double = 0 - let maxValue = data.reduce(0, +) - for slice in data { - let normalized:Double = Double(slice)/Double(maxValue) - let startDeg = lastEndDeg - let endDeg = lastEndDeg + (normalized * 360) - lastEndDeg = endDeg - tempSlices.append(PieSlice(startDeg: startDeg, endDeg: endDeg, value: slice, normalizedValue: normalized)) - } - return tempSlices - } - public var body: some View { - GeometryReader { geometry in - ZStack{ - ForEach(0.. Date: Sun, 24 May 2020 18:38:19 +0200 Subject: [PATCH 02/29] Added a first implementation of BarChart and LineChart also introduced style --- Sources/SwiftUICharts/BarChart/BarChart.swift | 7 - .../Base/Chart/AnyChartType.swift | 10 +- .../SwiftUICharts/Base/Chart/ChartType.swift | 2 +- .../Base/Chart/ChartTypeConfiguration.swift | 2 +- Sources/SwiftUICharts/Base/ChartView.swift | 11 +- .../Base/Extensions/CGPoint+Extension.swift | 35 ++ .../Base/Extensions/Path+QuadCurve.swift | 346 ++++++++++++++++++ .../Base/Extensions/View+Extension.swift | 11 + Sources/SwiftUICharts/Base/Style/Colors.swift | 3 + .../Charts/BarChart/BarChart.swift | 9 + .../Charts/BarChart/BarChartCell.swift | 28 ++ .../Charts/BarChart/BarChartRow.swift | 55 +++ .../{ => Charts}/BarChart/TitleLabel.swift | 0 .../Charts/LineChart/IndicatorPoint.swift | 28 ++ .../SwiftUICharts/Charts/LineChart/Line.swift | 107 ++++++ .../Charts/LineChart/LineChart.swift | 9 + .../Environment/EnvironmentKeys.swift | 4 +- 17 files changed, 649 insertions(+), 18 deletions(-) delete mode 100644 Sources/SwiftUICharts/BarChart/BarChart.swift create mode 100644 Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift create mode 100644 Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift create mode 100644 Sources/SwiftUICharts/Base/Extensions/View+Extension.swift create mode 100644 Sources/SwiftUICharts/Charts/BarChart/BarChart.swift create mode 100644 Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift create mode 100644 Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift rename Sources/SwiftUICharts/{ => Charts}/BarChart/TitleLabel.swift (100%) create mode 100644 Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift create mode 100644 Sources/SwiftUICharts/Charts/LineChart/Line.swift create mode 100644 Sources/SwiftUICharts/Charts/LineChart/LineChart.swift diff --git a/Sources/SwiftUICharts/BarChart/BarChart.swift b/Sources/SwiftUICharts/BarChart/BarChart.swift deleted file mode 100644 index c7a271b6..00000000 --- a/Sources/SwiftUICharts/BarChart/BarChart.swift +++ /dev/null @@ -1,7 +0,0 @@ -import SwiftUI - -public struct BarChart: ChartType { - public func makeChart(configuration: Self.Configuration) -> some View { - return Text("AAA") - } -} diff --git a/Sources/SwiftUICharts/Base/Chart/AnyChartType.swift b/Sources/SwiftUICharts/Base/Chart/AnyChartType.swift index f5f97bd7..046dd164 100644 --- a/Sources/SwiftUICharts/Base/Chart/AnyChartType.swift +++ b/Sources/SwiftUICharts/Base/Chart/AnyChartType.swift @@ -1,19 +1,19 @@ import SwiftUI struct AnyChartType: ChartType { - private let chartMaker: (ChartType.Configuration) -> AnyView + private let chartMaker: (ChartType.Configuration, ChartType.Style) -> AnyView init(_ type: S) { self.chartMaker = type.makeTypeErasedBody } - func makeChart(configuration: ChartType.Configuration) -> AnyView { - self.chartMaker(configuration) + func makeChart(configuration: ChartType.Configuration, style: ChartType.Style) -> AnyView { + self.chartMaker(configuration, style) } } fileprivate extension ChartType { - func makeTypeErasedBody(configuration: ChartType.Configuration) -> AnyView { - AnyView(makeChart(configuration: configuration)) + func makeTypeErasedBody(configuration: ChartType.Configuration, style: ChartType.Style) -> AnyView { + AnyView(makeChart(configuration: configuration, style: style)) } } diff --git a/Sources/SwiftUICharts/Base/Chart/ChartType.swift b/Sources/SwiftUICharts/Base/Chart/ChartType.swift index a432bae8..7951d29d 100644 --- a/Sources/SwiftUICharts/Base/Chart/ChartType.swift +++ b/Sources/SwiftUICharts/Base/Chart/ChartType.swift @@ -4,7 +4,7 @@ import SwiftUI public protocol ChartType { associatedtype Body : View - func makeChart(configuration: Self.Configuration, style: Style) -> Self.Body + func makeChart(configuration: Self.Configuration, style: Self.Style) -> Self.Body typealias Configuration = ChartTypeConfiguration typealias Style = ChartStyle diff --git a/Sources/SwiftUICharts/Base/Chart/ChartTypeConfiguration.swift b/Sources/SwiftUICharts/Base/Chart/ChartTypeConfiguration.swift index 1dbf8d80..0e56a54c 100644 --- a/Sources/SwiftUICharts/Base/Chart/ChartTypeConfiguration.swift +++ b/Sources/SwiftUICharts/Base/Chart/ChartTypeConfiguration.swift @@ -1,5 +1,5 @@ import SwiftUI public struct ChartTypeConfiguration { - public let data: [CGFloat] + public let data: [Double] } diff --git a/Sources/SwiftUICharts/Base/ChartView.swift b/Sources/SwiftUICharts/Base/ChartView.swift index 98353e9c..9a65c87d 100644 --- a/Sources/SwiftUICharts/Base/ChartView.swift +++ b/Sources/SwiftUICharts/Base/ChartView.swift @@ -8,8 +8,15 @@ public struct ChartView: View { @Environment(\.chartStyle) private var chartStyle @Environment(\.title) private var title + private var configuration: ChartTypeConfiguration + public var body: some View { - Text("aa") - } + self.chartType.makeChart(configuration: configuration, style: chartStyle) + } } +extension ChartView { + public init(data: [Double]) { + self.configuration = ChartTypeConfiguration(data: data) + } +} diff --git a/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift new file mode 100644 index 00000000..c063f2f5 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift @@ -0,0 +1,35 @@ +import SwiftUI + +extension CGPoint { + static func getStep(frame: CGRect, data: [Double]) -> CGPoint { + let padding: CGFloat = 30.0 + + // stepWidth + var stepWidth: CGFloat = 0.0 + if data.count < 2 { + stepWidth = 0.0 + } + stepWidth = frame.size.width / CGFloat(data.count - 1) + + // stepHeight + var stepHeight: CGFloat = 0.0 + + var min: Double? + var max: Double? + if let minPoint = data.min(), let maxPoint = data.max(), minPoint != maxPoint { + min = minPoint + max = maxPoint + }else { + return .zero + } + if let min = min, let max = max, min != max { + if (min <= 0){ + stepHeight = (frame.size.height-padding) / CGFloat(max - min) + }else{ + stepHeight = (frame.size.height-padding) / CGFloat(max + min) + } + } + + return CGPoint(x: stepWidth, y: stepHeight) + } +} diff --git a/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift b/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift new file mode 100644 index 00000000..f1828ff7 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift @@ -0,0 +1,346 @@ +import SwiftUI + +extension Path { + func trimmedPath(for percent: CGFloat) -> Path { + // percent difference between points + let boundsDistance: CGFloat = 0.001 + let completion: CGFloat = 1 - boundsDistance + + let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent) + + let start = pct > completion ? completion : pct - boundsDistance + let end = pct > completion ? 1 : pct + boundsDistance + return trimmedPath(from: start, to: end) + } + + func point(for percent: CGFloat) -> CGPoint { + let path = trimmedPath(for: percent) + return CGPoint(x: path.boundingRect.midX, y: path.boundingRect.midY) + } + + func point(to maxX: CGFloat) -> CGPoint { + let total = length + let sub = length(to: maxX) + let percent = sub / total + return point(for: percent) + } + + var length: CGFloat { + var ret: CGFloat = 0.0 + var start: CGPoint? + var point = CGPoint.zero + + forEach { ele in + switch ele { + case .move(let to): + if start == nil { + start = to + } + point = to + case .line(let to): + ret += point.line(to: to) + point = to + case .quadCurve(let to, let control): + ret += point.quadCurve(to: to, control: control) + point = to + case .curve(let to, let control1, let control2): + ret += point.curve(to: to, control1: control1, control2: control2) + point = to + case .closeSubpath: + if let to = start { + ret += point.line(to: to) + point = to + } + start = nil + } + } + return ret + } + + func length(to maxX: CGFloat) -> CGFloat { + var ret: CGFloat = 0.0 + var start: CGPoint? + var point = CGPoint.zero + var finished = false + + forEach { ele in + if finished { + return + } + switch ele { + case .move(let to): + if to.x > maxX { + finished = true + return + } + if start == nil { + start = to + } + point = to + case .line(let to): + if to.x > maxX { + finished = true + ret += point.line(to: to, x: maxX) + return + } + ret += point.line(to: to) + point = to + case .quadCurve(let to, let control): + if to.x > maxX { + finished = true + ret += point.quadCurve(to: to, control: control, x: maxX) + return + } + ret += point.quadCurve(to: to, control: control) + point = to + case .curve(let to, let control1, let control2): + if to.x > maxX { + finished = true + ret += point.curve(to: to, control1: control1, control2: control2, x: maxX) + return + } + ret += point.curve(to: to, control1: control1, control2: control2) + point = to + case .closeSubpath: + fatalError("Can't include closeSubpath") + } + } + return ret + } + + static func quadCurvedPathWithPoints(points:[Double], step:CGPoint, globalOffset: Double? = nil) -> Path { + var path = Path() + if (points.count < 2){ + return path + } + let offset = globalOffset ?? points.min()! +// guard let offset = points.min() else { return path } + var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y) + path.move(to: p1) + for pointIndex in 1.. Path { + var path = Path() + if (points.count < 2){ + return path + } + let offset = globalOffset ?? points.min()! + +// guard let offset = points.min() else { return path } + path.move(to: .zero) + var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y) + path.addLine(to: p1) + for pointIndex in 1.. Path { + var path = Path() + if (points.count < 2){ + return path + } + guard let offset = points.min() else { return path } + let p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y) + path.move(to: p1) + for pointIndex in 1.. Path { + var path = Path() + if (points.count < 2){ + return path + } + guard let offset = points.min() else { return path } + var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y) + path.move(to: p1) + for pointIndex in 1.. CGPoint { + let a = (to.y - self.y) / (to.x - self.x) + let y = self.y + (x - self.x) * a + return CGPoint(x: x, y: y) + } + + func line(to: CGPoint) -> CGFloat { + dist(to: to) + } + + func line(to: CGPoint, x: CGFloat) -> CGFloat { + dist(to: point(to: to, x: x)) + } + + func quadCurve(to: CGPoint, control: CGPoint) -> CGFloat { + var dist: CGFloat = 0 + let steps: CGFloat = 100 + + for i in 0.. CGFloat { + var dist: CGFloat = 0 + let steps: CGFloat = 100 + + for i in 0..= x { + return dist + } else if b.x > x { + dist += a.line(to: b, x: x) + return dist + } else if b.x == x { + dist += a.line(to: b) + return dist + } + + dist += a.line(to: b) + } + return dist + } + + func point(to: CGPoint, t: CGFloat, control: CGPoint) -> CGPoint { + let x = CGPoint.value(x: self.x, y: to.x, t: t, c: control.x) + let y = CGPoint.value(x: self.y, y: to.y, t: t, c: control.y) + + return CGPoint(x: x, y: y) + } + + func curve(to: CGPoint, control1: CGPoint, control2: CGPoint) -> CGFloat { + var dist: CGFloat = 0 + let steps: CGFloat = 100 + + for i in 0.. CGFloat { + var dist: CGFloat = 0 + let steps: CGFloat = 100 + + for i in 0..= x { + return dist + } else if b.x > x { + dist += a.line(to: b, x: x) + return dist + } else if b.x == x { + dist += a.line(to: b) + return dist + } + + dist += a.line(to: b) + } + + return dist + } + + func point(to: CGPoint, t: CGFloat, control1: CGPoint, control2: CGPoint) -> CGPoint { + let x = CGPoint.value(x: self.x, y: to.x, t: t, c1: control1.x, c2: control2.x) + let y = CGPoint.value(x: self.y, y: to.y, t: t, c1: control1.y, c2: control2.x) + + return CGPoint(x: x, y: y) + } + + static func value(x: CGFloat, y: CGFloat, t: CGFloat, c: CGFloat) -> CGFloat { + var value: CGFloat = 0.0 + // (1-t)^2 * p0 + 2 * (1-t) * t * c1 + t^2 * p1 + value += pow(1-t, 2) * x + value += 2 * (1-t) * t * c + value += pow(t, 2) * y + return value + } + + static func value(x: CGFloat, y: CGFloat, t: CGFloat, c1: CGFloat, c2: CGFloat) -> CGFloat { + var value: CGFloat = 0.0 + // (1-t)^3 * p0 + 3 * (1-t)^2 * t * c1 + 3 * (1-t) * t^2 * c2 + t^3 * p1 + value += pow(1-t, 3) * x + value += 3 * pow(1-t, 2) * t * c1 + value += 3 * (1-t) * pow(t, 2) * c2 + value += pow(t, 3) * y + return value + } + + static func getMidPoint(point1: CGPoint, point2: CGPoint) -> CGPoint { + return CGPoint( + x: point1.x + (point2.x - point1.x) / 2, + y: point1.y + (point2.y - point1.y) / 2 + ) + } + + func dist(to: CGPoint) -> CGFloat { + return sqrt((pow(self.x - to.x, 2) + pow(self.y - to.y, 2))) + } + + static func midPointForPoints(p1:CGPoint, p2:CGPoint) -> CGPoint { + return CGPoint(x:(p1.x + p2.x) / 2,y: (p1.y + p2.y) / 2) + } + + static func controlPointForPoints(p1:CGPoint, p2:CGPoint) -> CGPoint { + var controlPoint = CGPoint.midPointForPoints(p1:p1, p2:p2) + let diffY = abs(p2.y - controlPoint.y) + + if (p1.y < p2.y){ + controlPoint.y += diffY + } else if (p1.y > p2.y) { + controlPoint.y -= diffY + } + return controlPoint + } +} + diff --git a/Sources/SwiftUICharts/Base/Extensions/View+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/View+Extension.swift new file mode 100644 index 00000000..fbfc7fb2 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Extensions/View+Extension.swift @@ -0,0 +1,11 @@ +import SwiftUI + +extension View { + public func type(_ type: S) -> some View where S : ChartType { + self.environment(\.chartType, AnyChartType(type)) + } + + public func style(_ style: ChartStyle) -> some View { + self.environment(\.chartStyle, style) + } +} diff --git a/Sources/SwiftUICharts/Base/Style/Colors.swift b/Sources/SwiftUICharts/Base/Style/Colors.swift index 5180d305..9e3478be 100644 --- a/Sources/SwiftUICharts/Base/Style/Colors.swift +++ b/Sources/SwiftUICharts/Base/Style/Colors.swift @@ -4,4 +4,7 @@ public enum ChartColors { // Orange static let orangeBright = Color(hexString: "#FF782C") static let orangeDark = Color(hexString: "#EC2301") + + static let LegendColor:Color = Color(hexString: "#E8E7EA") + static let IndicatorKnob:Color = Color(hexString: "#FF57A6") } diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift new file mode 100644 index 00000000..41b2e594 --- /dev/null +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift @@ -0,0 +1,9 @@ +import SwiftUI + +public struct BarChart: ChartType { + public func makeChart(configuration: Self.Configuration, style: Self.Style) -> some View { + BarChartRow(data: configuration.data, gradientColor: style.foregroundColor) + } + + public init() {} +} diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift new file mode 100644 index 00000000..1ccaa6df --- /dev/null +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift @@ -0,0 +1,28 @@ +import SwiftUI + +public struct BarChartCell : View { + @State var value: Double + @State var index: Int = 0 + @State var width: Float + @State var numberOfDataPoints: Int + var gradientColor: ColorGradient + + var cellWidth: Double { + return Double(width)/(Double(numberOfDataPoints) * 1.5) + } + + @State var scaleValue: Double = 0 + @Binding var touchLocation: CGFloat + public var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 4) + .fill(LinearGradient(gradient: gradientColor.gradient, startPoint: .bottom, endPoint: .top)) + } + .frame(width: CGFloat(self.cellWidth)) + .scaleEffect(CGSize(width: 1, height: self.scaleValue), anchor: .bottom) + .onAppear(){ + self.scaleValue = self.value + } + .animation(Animation.spring().delay(self.touchLocation < 0 ? Double(self.index) * 0.04 : 0)) + } +} diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift new file mode 100644 index 00000000..524f3621 --- /dev/null +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift @@ -0,0 +1,55 @@ +import SwiftUI + +public struct BarChartRow : View { + @State var data: [Double] = [] + @State var touchLocation: CGFloat = -1.0 + + enum Constant { + static let spacing: CGFloat = 16.0 + } + + var gradientColor: ColorGradient + var maxValue: Double { + data.max() ?? 0 + } + + public var body: some View { + GeometryReader { geometry in + HStack(alignment: .bottom, + spacing: (geometry.frame(in: .local).width - Constant.spacing) / CGFloat(self.data.count * 3)) { + ForEach(0.. Double { + return Double(data[index])/Double(maxValue) + } + + func getScaleSize(touchLocation: CGFloat, index: Int) -> CGSize { + if touchLocation > CGFloat(index)/CGFloat(self.data.count) && + touchLocation < CGFloat(index+1)/CGFloat(self.data.count) { + return CGSize(width: 1.4, height: 1.1) + } + return CGSize(width: 1, height: 1) + } +} diff --git a/Sources/SwiftUICharts/BarChart/TitleLabel.swift b/Sources/SwiftUICharts/Charts/BarChart/TitleLabel.swift similarity index 100% rename from Sources/SwiftUICharts/BarChart/TitleLabel.swift rename to Sources/SwiftUICharts/Charts/BarChart/TitleLabel.swift diff --git a/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift b/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift new file mode 100644 index 00000000..5a3f5694 --- /dev/null +++ b/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift @@ -0,0 +1,28 @@ +// +// IndicatorPoint.swift +// LineChart +// +// Created by András Samu on 2019. 09. 03.. +// Copyright © 2019. András Samu. All rights reserved. +// + +import SwiftUI + +struct IndicatorPoint: View { + var body: some View { + ZStack{ + Circle() + .fill(ChartColors.IndicatorKnob) + Circle() + .stroke(Color.white, style: StrokeStyle(lineWidth: 4)) + } + .frame(width: 14, height: 14) + .shadow(color: ChartColors.LegendColor, radius: 6, x: 0, y: 6) + } +} + +struct IndicatorPoint_Previews: PreviewProvider { + static var previews: some View { + IndicatorPoint() + } +} diff --git a/Sources/SwiftUICharts/Charts/LineChart/Line.swift b/Sources/SwiftUICharts/Charts/LineChart/Line.swift new file mode 100644 index 00000000..58c4f16e --- /dev/null +++ b/Sources/SwiftUICharts/Charts/LineChart/Line.swift @@ -0,0 +1,107 @@ +import SwiftUI + +public struct Line: View { + @State var frame: CGRect = .zero + @State var data: [Double] + var gradientColor: ColorGradient + + @State var showIndicator: Bool = false + @State var touchLocation: CGPoint = .zero + @State private var showFull: Bool = false + @State var showBackground: Bool = true + var curvedLines: Bool = true + var step: CGPoint { + return CGPoint.getStep(frame: frame, data: data) + } + + var path: Path { + let points = data + + if curvedLines { + return Path.quadCurvedPathWithPoints(points: points, + step: step, + globalOffset: nil) + } + + return Path.linePathWithPoints(points: points, step: step) + } + + var closedPath: Path { + let points = data + + if curvedLines { + return Path.quadClosedCurvedPathWithPoints(points: points, + step: step, + globalOffset: nil) + } + + return Path.closedLinePathWithPoints(points: points, step: step) + } + + public var body: some View { + GeometryReader { geometry in + ZStack { + if self.showFull && self.showBackground { + self.getBackgroundPathView() + } + self.getLinePathView() + if(self.showIndicator) { + IndicatorPoint() + .position(self.getClosestPointOnPath(touchLocation: self.touchLocation)) + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + } + } + .onAppear() { + self.frame = geometry.frame(in: .local) + } + .gesture(DragGesture() + .onChanged({ value in + self.touchLocation = value.location + self.showIndicator = true + }) + .onEnded({ value in + self.touchLocation = .zero + self.showIndicator = false + }) + ) + } + } +} + +// MARK: - Private functions + +extension Line { + private func getClosestPointOnPath(touchLocation: CGPoint) -> CGPoint { + let closest = self.path.point(to: touchLocation.x) + return closest + } + + private func getBackgroundPathView() -> some View { + self.closedPath + .fill(LinearGradient(gradient: gradientColor.gradient, startPoint: .bottom, endPoint: .top)) + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + .transition(.opacity) + .animation(.easeIn(duration: 1.6)) + } + + private func getLinePathView() -> some View { + self.path + .trim(from: 0, to: self.showFull ? 1:0) + .stroke(LinearGradient(gradient: gradientColor.gradient, + startPoint: .leading, + endPoint: .trailing), + style: StrokeStyle(lineWidth: 3, lineJoin: .round)) + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + .animation(Animation.easeOut(duration: 1.2)) + .onAppear { + self.showFull = true + } + .onDisappear { + self.showFull = false + } + .drawingGroup() + } +} diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift b/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift new file mode 100644 index 00000000..cd3cb8c9 --- /dev/null +++ b/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift @@ -0,0 +1,9 @@ +import SwiftUI + +public struct LineChart: ChartType { + public func makeChart(configuration: Self.Configuration, style: Self.Style) -> some View { + Line(data: configuration.data, gradientColor: style.foregroundColor) + } + + public init() {} +} diff --git a/Sources/SwiftUICharts/Environment/EnvironmentKeys.swift b/Sources/SwiftUICharts/Environment/EnvironmentKeys.swift index 18983e5d..f67f48e3 100644 --- a/Sources/SwiftUICharts/Environment/EnvironmentKeys.swift +++ b/Sources/SwiftUICharts/Environment/EnvironmentKeys.swift @@ -6,8 +6,8 @@ struct ChartTypeKey: EnvironmentKey { struct ChartStyleKey: EnvironmentKey { static let defaultValue: ChartStyle = ChartStyle(backgroundColor: .white, - foregroundColor: ColorGradient(ChartColors.orangeBright, - ChartColors.orangeDark)) + foregroundColor: ColorGradient(ChartColors.orangeDark, + ChartColors.orangeBright)) } struct ChartLabelKey: EnvironmentKey { From a2d75dca0ee5299cfbd3e38209601479497f6cd8 Mon Sep 17 00:00:00 2001 From: Adrian Date: Mon, 25 May 2020 03:36:47 -0400 Subject: [PATCH 03/29] Write unit tests for CGPoint+Extension.swift (#100) - write unit tests for CGPoint+Extension.swift - clean up formatting on CGPoint+Extension.swift --- .../Base/Extensions/CGPoint+Extension.swift | 10 +++--- .../CGPointExtensionTests.swift | 32 +++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 Tests/SwiftUIChartsTests/CGPointExtensionTests.swift diff --git a/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift index c063f2f5..f8241413 100644 --- a/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift @@ -19,14 +19,14 @@ extension CGPoint { if let minPoint = data.min(), let maxPoint = data.max(), minPoint != maxPoint { min = minPoint max = maxPoint - }else { + } else { return .zero } if let min = min, let max = max, min != max { - if (min <= 0){ - stepHeight = (frame.size.height-padding) / CGFloat(max - min) - }else{ - stepHeight = (frame.size.height-padding) / CGFloat(max + min) + if (min <= 0) { + stepHeight = (frame.size.height - padding) / CGFloat(max - min) + } else { + stepHeight = (frame.size.height - padding) / CGFloat(max + min) } } diff --git a/Tests/SwiftUIChartsTests/CGPointExtensionTests.swift b/Tests/SwiftUIChartsTests/CGPointExtensionTests.swift new file mode 100644 index 00000000..692e9c77 --- /dev/null +++ b/Tests/SwiftUIChartsTests/CGPointExtensionTests.swift @@ -0,0 +1,32 @@ +// +// CGPointExtensionTests.swift +// SwiftUIChartsTests +// +// Created by Adrian Bolinger on 5/24/20. +// + +@testable import SwiftUICharts +import XCTest + +class CGPointExtensionTests: XCTestCase { + static let twentyElementArray: [Double] = Array(repeating: Double.random(in: 1...100), count: 20) + + func testGetStepWithOneElementArray() { + let frame = CGRect(x: 0, y: 0, width: 300, height: 300) + let oneElementArray: [Double] = [0.0] + + XCTAssertEqual(CGPoint.getStep(frame: frame, data: oneElementArray), .zero) + } + + func testGetStepWithMultiElementArrayWithNegativeValues() { + let frame = CGRect(x: 0, y: 0, width: 300, height: 300) + let multiElementArray: [Double] = [-5.0, 0.0, 5.0] + XCTAssertEqual(CGPoint.getStep(frame: frame, data: multiElementArray), CGPoint(x: 150.0, y: 27.0)) + } + + func testGetStepWithMultiElementArrayWithPositiveValues() { + let frame = CGRect(x: 0, y: 0, width: 300, height: 300) + let multiElementArray: [Double] = [5.0, 10.0, 15.0] + XCTAssertEqual(CGPoint.getStep(frame: frame, data: multiElementArray), CGPoint(x: 150.0, y: 13.5)) + } +} From aa9126482f00cc57587614ffcd8561103172da31 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 25 May 2020 08:20:46 -0400 Subject: [PATCH 04/29] Add PieChart + multicolor (#98) * Add ColorGradient example Add ColorGradient single color constructor Add preview for BarChart * Add PieChart Allow multi color for Pie and Bar Add linter --- .swiftlint.yml | 64 +++++++++++ Package.swift | 6 +- .../Base/Extensions/CGRect+Extension.swift | 16 +++ .../Base/Extensions/Color+Extension.swift | 12 +- .../SwiftUICharts/Base/Style/ChartStyle.swift | 21 +++- .../Base/Style/ColorGradient.swift | 23 ++++ .../Charts/BarChart/BarChart.swift | 22 +++- .../Charts/BarChart/BarChartCell.swift | 26 ++++- .../Charts/BarChart/BarChartRow.swift | 37 ++++-- .../SwiftUICharts/Charts/LineChart/Line.swift | 18 ++- .../Charts/LineChart/LineChart.swift | 22 +++- Sources/SwiftUICharts/PieChart/PieChart.swift | 59 ++++++++++ .../SwiftUICharts/PieChart/PieChartCell.swift | 107 ++++++++++++++++++ .../SwiftUICharts/PieChart/PieChartRow.swift | 66 +++++++++++ 14 files changed, 472 insertions(+), 27 deletions(-) create mode 100644 .swiftlint.yml create mode 100644 Sources/SwiftUICharts/Base/Extensions/CGRect+Extension.swift create mode 100644 Sources/SwiftUICharts/PieChart/PieChart.swift create mode 100644 Sources/SwiftUICharts/PieChart/PieChartCell.swift create mode 100644 Sources/SwiftUICharts/PieChart/PieChartRow.swift diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 00000000..60e30109 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,64 @@ +disabled_rules: +- explicit_acl +- trailing_whitespace +- force_cast +- unused_closure_parameter +- multiple_closures_with_trailing_closure +opt_in_rules: +- anyobject_protocol +- array_init +- attributes +- collection_alignment +- colon +- conditional_returns_on_newline +- convenience_type +- empty_count +- empty_string +- empty_collection_literal +- enum_case_associated_values_count +- function_default_parameter_at_end +- fatal_error_message +- file_name +- first_where +- modifier_order +- toggle_bool +- unused_private_declaration +- yoda_condition +excluded: +- Carthage +- Pods +- SwiftLint/Common/3rdPartyLib +identifier_name: + excluded: + - a + - b + - c + - i + - id + - t + - to + - x + - y +line_length: + warning: 150 + error: 200 + ignores_function_declarations: true + ignores_comments: true + ignores_urls: true +function_body_length: + warning: 300 + error: 500 +function_parameter_count: + warning: 6 + error: 8 +type_body_length: + warning: 300 + error: 400 +file_length: + warning: 500 + error: 1200 + ignore_comment_only_lines: true +cyclomatic_complexity: + warning: 15 + error: 21 +reporter: "xcode" diff --git a/Package.swift b/Package.swift index ffd10e06..5a466752 100644 --- a/Package.swift +++ b/Package.swift @@ -6,13 +6,13 @@ import PackageDescription let package = Package( name: "SwiftUICharts", platforms: [ - .iOS(.v13),.watchOS(.v6) + .iOS(.v13), .watchOS(.v6) ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( name: "SwiftUICharts", - targets: ["SwiftUICharts"]), + targets: ["SwiftUICharts"]) ], dependencies: [ // Dependencies declare other packages that this package depends on. @@ -26,6 +26,6 @@ let package = Package( dependencies: []), .testTarget( name: "SwiftUIChartsTests", - dependencies: ["SwiftUICharts"]), + dependencies: ["SwiftUICharts"]) ] ) diff --git a/Sources/SwiftUICharts/Base/Extensions/CGRect+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/CGRect+Extension.swift new file mode 100644 index 00000000..c84d8374 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Extensions/CGRect+Extension.swift @@ -0,0 +1,16 @@ +// +// CGRect+Extension.swift +// SwiftUICharts +// +// Created by Nicolas Savoini on 2020-05-24. +// + +import Foundation +import SwiftUI + +extension CGRect { + // Return the coordinate for a rectangle center + public var mid: CGPoint { + return CGPoint(x: self.midX, y: self.midY) + } +} diff --git a/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift index e8b24983..a018c3c9 100644 --- a/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift @@ -5,17 +5,17 @@ extension Color { let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) var int = UInt64() Scanner(string: hex).scanHexInt64(&int) - let r, g, b: UInt64 + let red, green, blue: UInt64 switch hex.count { case 3: // RGB (12-bit) - (r, g, b) = ((int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + (red, green, blue) = ((int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) case 6: // RGB (24-bit) - (r, g, b) = (int >> 16, int >> 8 & 0xFF, int & 0xFF) + (red, green, blue) = (int >> 16, int >> 8 & 0xFF, int & 0xFF) case 8: // ARGB (32-bit) - (r, g, b) = (int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + (red, green, blue) = (int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) default: - (r, g, b) = (0, 0, 0) + (red, green, blue) = (0, 0, 0) } - self.init(red: Double(r) / 255, green: Double(g) / 255, blue: Double(b) / 255) + self.init(red: Double(red) / 255, green: Double(green) / 255, blue: Double(blue) / 255) } } diff --git a/Sources/SwiftUICharts/Base/Style/ChartStyle.swift b/Sources/SwiftUICharts/Base/Style/ChartStyle.swift index b7474f8c..e23ede3a 100644 --- a/Sources/SwiftUICharts/Base/Style/ChartStyle.swift +++ b/Sources/SwiftUICharts/Base/Style/ChartStyle.swift @@ -1,11 +1,28 @@ import SwiftUI public struct ChartStyle { - public let backgroundColor: Color - public let foregroundColor: ColorGradient + public let backgroundColor: ColorGradient + public let foregroundColor: [ColorGradient] + + public init(backgroundColor: Color, foregroundColor: [ColorGradient]) { + self.backgroundColor = ColorGradient.init(backgroundColor) + self.foregroundColor = foregroundColor + } + public init(backgroundColor: Color, foregroundColor: ColorGradient) { + self.backgroundColor = ColorGradient.init(backgroundColor) + self.foregroundColor = [foregroundColor] + } + + public init(backgroundColor: ColorGradient, foregroundColor: ColorGradient) { + self.backgroundColor = backgroundColor + self.foregroundColor = [foregroundColor] + } + + public init(backgroundColor: ColorGradient, foregroundColor: [ColorGradient]) { self.backgroundColor = backgroundColor self.foregroundColor = foregroundColor } + } diff --git a/Sources/SwiftUICharts/Base/Style/ColorGradient.swift b/Sources/SwiftUICharts/Base/Style/ColorGradient.swift index 18a4ab20..827e0096 100644 --- a/Sources/SwiftUICharts/Base/Style/ColorGradient.swift +++ b/Sources/SwiftUICharts/Base/Style/ColorGradient.swift @@ -4,6 +4,11 @@ public struct ColorGradient { public let startColor: Color public let endColor: Color + public init(_ color: Color) { + self.startColor = color + self.endColor = color + } + public init (_ startColor: Color, _ endColor: Color) { self.startColor = startColor self.endColor = endColor @@ -13,3 +18,21 @@ public struct ColorGradient { return Gradient(colors: [startColor, endColor]) } } + +extension ColorGradient { + /// Convenience method to return a LinearGradient from the ColorGradient + /// - Parameters: + /// - startPoint: starting point + /// - endPoint: ending point + /// - Returns: a Linear gradient + public func linearGradient(from startPoint: UnitPoint, to endPoint: UnitPoint) -> LinearGradient { + return LinearGradient(gradient: self.gradient, startPoint: startPoint, endPoint: endPoint) + } +} + +extension ColorGradient { + public static let orangeBright = ColorGradient(ChartColors.orangeBright) + public static let redBlack = ColorGradient(.red, .black) + public static let greenRed = ColorGradient(.green, .red) + public static let whiteBlack = ColorGradient(.white, .black) +} diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift index 41b2e594..d91d1db0 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift @@ -2,8 +2,26 @@ import SwiftUI public struct BarChart: ChartType { public func makeChart(configuration: Self.Configuration, style: Self.Style) -> some View { - BarChartRow(data: configuration.data, gradientColor: style.foregroundColor) + BarChartRow(data: configuration.data, style: style) } - public init() {} } + +struct BarChart_Previews: PreviewProvider { + static var previews: some View { + Group { + Group { + BarChart().makeChart( + configuration: .init(data: [1, 2, 3, 5, 1]), + style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack)) + }.environment(\.colorScheme, .light) + + Group { + BarChart().makeChart( + configuration: .init(data: [1, 2, 3]), + style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack)) + }.environment(\.colorScheme, .dark) + + } + } +} diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift index 1ccaa6df..22f2301b 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift @@ -1,6 +1,6 @@ import SwiftUI -public struct BarChartCell : View { +public struct BarChartCell: View { @State var value: Double @State var index: Int = 0 @State var width: Float @@ -13,16 +13,36 @@ public struct BarChartCell : View { @State var scaleValue: Double = 0 @Binding var touchLocation: CGFloat + public var body: some View { ZStack { RoundedRectangle(cornerRadius: 4) - .fill(LinearGradient(gradient: gradientColor.gradient, startPoint: .bottom, endPoint: .top)) + .fill(gradientColor.linearGradient(from: .bottom, to: .top)) } .frame(width: CGFloat(self.cellWidth)) .scaleEffect(CGSize(width: 1, height: self.scaleValue), anchor: .bottom) - .onAppear(){ + .onAppear { self.scaleValue = self.value } .animation(Animation.spring().delay(self.touchLocation < 0 ? Double(self.index) * 0.04 : 0)) } } + +struct BarChartCell_Previews: PreviewProvider { + static var previews: some View { + Group { + Group { + BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: .constant(CGFloat())) + BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: .constant(CGFloat())) + BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient(.purple), touchLocation: .constant(CGFloat())) + } + + Group { + BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: .constant(CGFloat())) + BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: .constant(CGFloat())) + BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient(.purple), touchLocation: .constant(CGFloat())) + }.environment(\.colorScheme, .dark) + } + + } +} diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift index 524f3621..4a517597 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift @@ -1,6 +1,6 @@ import SwiftUI -public struct BarChartRow : View { +public struct BarChartRow: View { @State var data: [Double] = [] @State var touchLocation: CGFloat = -1.0 @@ -8,7 +8,8 @@ public struct BarChartRow : View { static let spacing: CGFloat = 16.0 } - var gradientColor: ColorGradient + var style: ChartStyle + var maxValue: Double { data.max() ?? 0 } @@ -17,14 +18,14 @@ public struct BarChartRow : View { GeometryReader { geometry in HStack(alignment: .bottom, spacing: (geometry.frame(in: .local).width - Constant.spacing) / CGFloat(self.data.count * 3)) { - ForEach(0.. some View { self.closedPath - .fill(LinearGradient(gradient: gradientColor.gradient, startPoint: .bottom, endPoint: .top)) + .fill(style.backgroundColor.linearGradient(from: .bottom, to: .top)) .rotationEffect(.degrees(180), anchor: .center) .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) .transition(.opacity) @@ -89,7 +89,7 @@ extension Line { private func getLinePathView() -> some View { self.path .trim(from: 0, to: self.showFull ? 1:0) - .stroke(LinearGradient(gradient: gradientColor.gradient, + .stroke(LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient, startPoint: .leading, endPoint: .trailing), style: StrokeStyle(lineWidth: 3, lineJoin: .round)) @@ -105,3 +105,15 @@ extension Line { .drawingGroup() } } + +struct Line_Previews: PreviewProvider { + static var previews: some View { + Group { + Line(data: [1, 2, 3, 1, 2, 5, 7], style: blackLineStyle) + Line(data: [1, 2, 3, 1, 2, 5, 7], style: redLineStyle) + } + } +} + +private let blackLineStyle = ChartStyle(backgroundColor: ColorGradient(.white), foregroundColor: ColorGradient(.black)) +private let redLineStyle = ChartStyle(backgroundColor: .whiteBlack, foregroundColor: ColorGradient(.red)) diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift b/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift index cd3cb8c9..5bd76709 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift @@ -2,8 +2,28 @@ import SwiftUI public struct LineChart: ChartType { public func makeChart(configuration: Self.Configuration, style: Self.Style) -> some View { - Line(data: configuration.data, gradientColor: style.foregroundColor) + Line(data: configuration.data, style: style) } public init() {} } + +struct LineChart_Previews: PreviewProvider { + static var previews: some View { + Group { + Group { + LineChart().makeChart( + configuration: .init(data: [1, 2, 3, 5, 1]), + style: .init(backgroundColor: .white, foregroundColor: ColorGradient(.black))) + }.environment(\.colorScheme, .light) + + Group { + LineChart().makeChart( + configuration: .init(data: [1, 2, 3]), + style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack)) + }.environment(\.colorScheme, .dark) + + } + + } +} diff --git a/Sources/SwiftUICharts/PieChart/PieChart.swift b/Sources/SwiftUICharts/PieChart/PieChart.swift new file mode 100644 index 00000000..9794fda8 --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/PieChart.swift @@ -0,0 +1,59 @@ +// +// PieChart.swift +// SwiftUICharts +// +// Created by Nicolas Savoini on 2020-05-24. +// + +import SwiftUI + +public struct PieChart: ChartType { + public func makeChart(configuration: Self.Configuration, style: Self.Style) -> some View { + PieChartRow(data: configuration.data, style: style) + } + public init() {} +} + +struct PieChart_Previews: PreviewProvider { + static var previews: some View { + Group { + Group { + PieChart().makeChart( + configuration: .init(data: [56, 78, 53, 65, 54]), + style: styleOneColor) + PieChart().makeChart( + configuration: .init(data: [56, 78, 53, 65, 54]), + style: styleTwoColor) + PieChart().makeChart( + configuration: .init(data: [1, 1, 1, 1, 1, 1]), + style: trivialPursuit) + }.environment(\.colorScheme, .light) + + Group { + PieChart().makeChart( + configuration: .init(data: [56, 78, 53, 65, 54]), + style: styleOneColor) + PieChart().makeChart( + configuration: .init(data: [56, 78, 53, 65, 54]), + style: styleTwoColor) + PieChart().makeChart( + configuration: .init(data: [1, 1, 1, 1, 1, 1]), + style: trivialPursuit) + }.environment(\.colorScheme, .dark) + + }.previewLayout(.fixed(width: 250, height: 400)) + } +} + +private let styleOneColor = ChartStyle(backgroundColor: .white, foregroundColor: ColorGradient.init(.pink)) + +private let styleTwoColor = ChartStyle(backgroundColor: ColorGradient(.black), foregroundColor: [ColorGradient(.yellow), ColorGradient(.red)]) + +private let trivialPursuit = ChartStyle( + backgroundColor: .yellow, + foregroundColor: [ColorGradient(.yellow), + ColorGradient(.pink), + ColorGradient(.green), + ColorGradient(.primary), + ColorGradient(.blue), + ColorGradient(.orange)]) diff --git a/Sources/SwiftUICharts/PieChart/PieChartCell.swift b/Sources/SwiftUICharts/PieChart/PieChartCell.swift new file mode 100644 index 00000000..20157677 --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/PieChartCell.swift @@ -0,0 +1,107 @@ +// +// PieChartCell.swift +// SwiftUICharts +// +// Created by Nicolas Savoini on 2020-05-24. +// + +import SwiftUI + +struct PieSlice: Identifiable { + var id = UUID() + var startDeg: Double + var endDeg: Double + var value: Double + //var normalizedValue: Double +} + +public struct PieChartCell: View { + @State private var show: Bool = false + var rect: CGRect + var radius: CGFloat { + return min(rect.width, rect.height)/2 + } + var startDeg: Double + var endDeg: Double + var path: Path { + var path = Path() + path.addArc( + center: rect.mid, + radius: self.radius, + startAngle: Angle(degrees: self.startDeg), + endAngle: Angle(degrees: self.endDeg), + clockwise: false) + path.addLine(to: rect.mid) + path.closeSubpath() + return path + } + var index: Int + + // Section line border color + var backgroundColor: Color + + // Section color + var accentColor: ColorGradient + + public var body: some View { + Group { + path + .fill(self.accentColor.linearGradient(from: .bottom, to: .top)) + .overlay(path.stroke(self.backgroundColor, lineWidth: 2)) + .scaleEffect(self.show ? 1 : 0) + .animation(Animation.spring().delay(Double(self.index) * 0.04)) + .onAppear { + self.show = true + } + + } + } +} + +struct PieChartCell_Previews: PreviewProvider { + static var previews: some View { + Group { + + GeometryReader { geometry in + PieChartCell( + rect: geometry.frame(in: .local), + startDeg: 00.0, + endDeg: 90.0, + index: 0, + backgroundColor: Color.red, + accentColor: ColorGradient.greenRed) + }.frame(width: 100, height: 100) + + GeometryReader { geometry in + PieChartCell( + rect: geometry.frame(in: .local), + startDeg: 0.0, + endDeg: 90.0, + index: 0, + backgroundColor: Color.green, + accentColor: ColorGradient.redBlack) + }.frame(width: 100, height: 100) + + GeometryReader { geometry in + PieChartCell( + rect: geometry.frame(in: .local), + startDeg: 100.0, + endDeg: 135.0, + index: 0, + backgroundColor: Color.black, + accentColor: ColorGradient.whiteBlack) + }.frame(width: 100, height: 100) + + GeometryReader { geometry in + PieChartCell( + rect: geometry.frame(in: .local), + startDeg: 185.0, + endDeg: 290.0, + index: 0, + backgroundColor: Color.purple, + accentColor: ColorGradient(.purple)) + }.frame(width: 100, height: 100) + + }.previewLayout(.fixed(width: 125, height: 125)) + } +} diff --git a/Sources/SwiftUICharts/PieChart/PieChartRow.swift b/Sources/SwiftUICharts/PieChart/PieChartRow.swift new file mode 100644 index 00000000..4915bd97 --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/PieChartRow.swift @@ -0,0 +1,66 @@ +// +// PieChartRow.swift +// SwiftUICharts +// +// Created by Nicolas Savoini on 2020-05-24. +// + +import SwiftUI + +public struct PieChartRow: View { + var data: [Double] + + var style: ChartStyle + + var slices: [PieSlice] { + var tempSlices: [PieSlice] = [] + var lastEndDeg: Double = 0 + let maxValue = data.reduce(0, +) + + for slice in data { + let normalized: Double = Double(slice)/Double(maxValue) + let startDeg = lastEndDeg + let endDeg = lastEndDeg + (normalized * 360) + lastEndDeg = endDeg + tempSlices.append(PieSlice(startDeg: startDeg, endDeg: endDeg, value: slice)) + } + + return tempSlices + } + + public var body: some View { + GeometryReader { geometry in + ZStack { + ForEach(0.. Date: Mon, 25 May 2020 14:50:01 +0200 Subject: [PATCH 05/29] removed .rotate for foreGroundColor --- Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift | 2 +- Sources/SwiftUICharts/PieChart/PieChartRow.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift index 4a517597..9abe01aa 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift @@ -23,7 +23,7 @@ public struct BarChartRow: View { index: index, width: Float(geometry.frame(in: .local).width - Constant.spacing), numberOfDataPoints: self.data.count, - gradientColor: self.style.foregroundColor.rotate(for: index), + gradientColor: self.style.foregroundColor.first!, touchLocation: self.$touchLocation) .scaleEffect(self.getScaleSize(touchLocation: self.touchLocation, index: index), anchor: .bottom) .animation(.spring()) diff --git a/Sources/SwiftUICharts/PieChart/PieChartRow.swift b/Sources/SwiftUICharts/PieChart/PieChartRow.swift index 4915bd97..b4d8d003 100644 --- a/Sources/SwiftUICharts/PieChart/PieChartRow.swift +++ b/Sources/SwiftUICharts/PieChart/PieChartRow.swift @@ -38,7 +38,7 @@ public struct PieChartRow: View { endDeg: self.slices[index].endDeg, index: index, backgroundColor: self.style.backgroundColor.startColor, - accentColor: self.style.foregroundColor.rotate(for: index) + accentColor: self.style.foregroundColor.first! ) } From 99b952fcf46df30582e2bac94446f65664b80d74 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 26 May 2020 17:04:51 -0400 Subject: [PATCH 06/29] Restore rotating index for multicolor (#102) --- .../Base/Extensions/Array+Extension.swift | 22 +++++++++ .../Base/Style/ColorGradient.swift | 2 +- .../Charts/BarChart/BarChartRow.swift | 2 +- .../SwiftUICharts/PieChart/PieChartRow.swift | 12 ++++- .../ArrayExtensionTests.swift | 45 +++++++++++++++++++ 5 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift create mode 100644 Tests/SwiftUIChartsTests/ArrayExtensionTests.swift diff --git a/Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift new file mode 100644 index 00000000..f2898316 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift @@ -0,0 +1,22 @@ +// +// File.swift +// +// +// Created by Nicolas Savoini on 2020-05-25. +// + +import Foundation + +extension Array where Element == ColorGradient { + func rotate(for index: Int) -> ColorGradient { + if self.isEmpty { + return ColorGradient.orangeBright + } + + if self.count <= index { + return self[index % self.count] + } + + return self[index] + } +} diff --git a/Sources/SwiftUICharts/Base/Style/ColorGradient.swift b/Sources/SwiftUICharts/Base/Style/ColorGradient.swift index 827e0096..5446cd7d 100644 --- a/Sources/SwiftUICharts/Base/Style/ColorGradient.swift +++ b/Sources/SwiftUICharts/Base/Style/ColorGradient.swift @@ -1,6 +1,6 @@ import SwiftUI -public struct ColorGradient { +public struct ColorGradient: Equatable { public let startColor: Color public let endColor: Color diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift index 9abe01aa..4a517597 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift @@ -23,7 +23,7 @@ public struct BarChartRow: View { index: index, width: Float(geometry.frame(in: .local).width - Constant.spacing), numberOfDataPoints: self.data.count, - gradientColor: self.style.foregroundColor.first!, + gradientColor: self.style.foregroundColor.rotate(for: index), touchLocation: self.$touchLocation) .scaleEffect(self.getScaleSize(touchLocation: self.touchLocation, index: index), anchor: .bottom) .animation(.spring()) diff --git a/Sources/SwiftUICharts/PieChart/PieChartRow.swift b/Sources/SwiftUICharts/PieChart/PieChartRow.swift index b4d8d003..f9428179 100644 --- a/Sources/SwiftUICharts/PieChart/PieChartRow.swift +++ b/Sources/SwiftUICharts/PieChart/PieChartRow.swift @@ -38,7 +38,7 @@ public struct PieChartRow: View { endDeg: self.slices[index].endDeg, index: index, backgroundColor: self.style.backgroundColor.startColor, - accentColor: self.style.foregroundColor.first! + accentColor: self.style.foregroundColor.rotate(for: index) ) } @@ -56,6 +56,12 @@ struct PieChartRow_Previews: PreviewProvider { data: [8, 23, 32, 7, 23, 43], style: defaultMultiColorChartStyle) .frame(width: 100, height: 100) + + PieChartRow( + data: [8, 23, 32, 7, 23, 43], + style: multiColorChartStyle) + . frame(width: 100, height: 100) + }.previewLayout(.fixed(width: 125, height: 125)) } @@ -64,3 +70,7 @@ struct PieChartRow_Previews: PreviewProvider { private let defaultMultiColorChartStyle = ChartStyle( backgroundColor: Color.white, foregroundColor: [ColorGradient]()) + +private let multiColorChartStyle = ChartStyle( +backgroundColor: Color.purple, +foregroundColor: [ColorGradient.greenRed, ColorGradient.whiteBlack]) diff --git a/Tests/SwiftUIChartsTests/ArrayExtensionTests.swift b/Tests/SwiftUIChartsTests/ArrayExtensionTests.swift new file mode 100644 index 00000000..35b1e304 --- /dev/null +++ b/Tests/SwiftUIChartsTests/ArrayExtensionTests.swift @@ -0,0 +1,45 @@ +// +// File.swift +// +// +// Created by Nicolas Savoini on 2020-05-25. +// + +@testable import SwiftUICharts +import XCTest + +class ArrayExtensionTests: XCTestCase { + + func testArrayRotatingIndexEmpty() { + let colors = [ColorGradient]() + XCTAssertEqual(colors.rotate(for: 0), ColorGradient.orangeBright) + } + + func testArrayRotatingIndexOneValue() { + let colors = [ColorGradient.greenRed] + + XCTAssertEqual(colors.rotate(for: 0), ColorGradient.greenRed) + XCTAssertEqual(colors.rotate(for: 1), ColorGradient.greenRed) + XCTAssertEqual(colors.rotate(for: 2), ColorGradient.greenRed) + } + + func testArrayRotatingIndexLessValues() { + let colors = [ColorGradient.greenRed, ColorGradient.whiteBlack] + + XCTAssertEqual(colors.rotate(for: 0), ColorGradient.greenRed) + XCTAssertEqual(colors.rotate(for: 1), ColorGradient.whiteBlack) + XCTAssertEqual(colors.rotate(for: 2), ColorGradient.greenRed) + XCTAssertEqual(colors.rotate(for: 3), ColorGradient.whiteBlack) + XCTAssertEqual(colors.rotate(for: 4), ColorGradient.greenRed) + } + + func testArrayRotatingIndexMoreValues() { + let colors = [ColorGradient.greenRed, ColorGradient.whiteBlack, ColorGradient.orangeBright] + + XCTAssertEqual(colors.rotate(for: 0), ColorGradient.greenRed) + XCTAssertEqual(colors.rotate(for: 1), ColorGradient.whiteBlack) + + } + +} + From 4963ec54ed7d2ee6c90cdb6330b5ae37015e3c2f Mon Sep 17 00:00:00 2001 From: Adrian Date: Tue, 26 May 2020 17:05:05 -0400 Subject: [PATCH 07/29] Write unit tests for Color+Extension.swift (#101) - Wrote tests - Possible minor bug where there's an alpha missing on 32-bit colors? --- .../Base/Extensions/Color+Extension.swift | 2 + .../ColorExtensionTests.swift | 56 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 Tests/SwiftUIChartsTests/ColorExtensionTests.swift diff --git a/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift index a018c3c9..8742decf 100644 --- a/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift @@ -12,6 +12,8 @@ extension Color { case 6: // RGB (24-bit) (red, green, blue) = (int >> 16, int >> 8 & 0xFF, int & 0xFF) case 8: // ARGB (32-bit) + // FIXME: I think we need an an alpha value on this one. See link below. + // https://stackoverflow.com/a/56874327/4475605 (red, green, blue) = (int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) default: (red, green, blue) = (0, 0, 0) diff --git a/Tests/SwiftUIChartsTests/ColorExtensionTests.swift b/Tests/SwiftUIChartsTests/ColorExtensionTests.swift new file mode 100644 index 00000000..2a016ed2 --- /dev/null +++ b/Tests/SwiftUIChartsTests/ColorExtensionTests.swift @@ -0,0 +1,56 @@ +// +// ColorExtensionTests.swift +// SwiftUIChartsTests +// +// Created by Adrian Bolinger on 5/24/20. +// + +@testable import SwiftUICharts +import SwiftUI +import XCTest + +class ColorExtensionTests: XCTestCase { + func testTwentyFourBitRGBColors() { + let actualWhite = Color(hexString: "FFFFFF") + let expectedWhite = Color(red: 1, green: 1, blue: 1) + XCTAssertEqual(actualWhite, expectedWhite) + + let actualBlack = Color(hexString: "000000") + let expectedBlack = Color(red: 0, green: 0, blue: 0) + XCTAssertEqual(actualBlack, expectedBlack) + + let actualRed = Color(hexString: "FF0000") + let expectedRed = Color(red: 255/255, green: 0, blue: 0) + XCTAssertEqual(actualRed, expectedRed) + + let actualGreen = Color(hexString: "00FF00") + let expectedGreen = Color(red: 0, green: 1, blue: 0) + XCTAssertEqual(actualGreen, expectedGreen) + + let actualBlue = Color(hexString: "0000FF") + let expectedBlue = Color(red: 0, green: 0, blue: 1) + XCTAssertEqual(actualBlue, expectedBlue) + } + + func testTwelveBitRGBColors() { + let actualWhite = Color(hexString: "FFF") + let expectedWhite = Color(red: 1, green: 1, blue: 1) + XCTAssertEqual(actualWhite, expectedWhite) + + let actualBlack = Color(hexString: "000") + let expectedBlack = Color(red: 0, green: 0, blue: 0) + XCTAssertEqual(actualBlack, expectedBlack) + + let actualRed = Color(hexString: "F00") + let expectedRed = Color(red: 255/255, green: 0, blue: 0) + XCTAssertEqual(actualRed, expectedRed) + + let actualGreen = Color(hexString: "0F0") + let expectedGreen = Color(red: 0, green: 1, blue: 0) + XCTAssertEqual(actualGreen, expectedGreen) + + let actualBlue = Color(hexString: "00F") + let expectedBlue = Color(red: 0, green: 0, blue: 1) + XCTAssertEqual(actualBlue, expectedBlue) + } +} From f2866ae2811bc78360ef8a0bdea827b31edd93d1 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 28 May 2020 04:54:06 -0400 Subject: [PATCH 08/29] Syntax corrections (#105) * Movie PieChartFile * Variable renaming --- .../SwiftUICharts/Base/Chart/ChartType.swift | 2 +- .../Base/Extensions/CGPoint+Extension.swift | 2 +- .../Base/Extensions/Path+QuadCurve.swift | 99 ++++++++++--------- .../Base/Extensions/View+Extension.swift | 2 +- .../SwiftUICharts/Base/Label/ChartLabel.swift | 2 +- Sources/SwiftUICharts/Base/Style/Colors.swift | 4 +- .../Charts/LineChart/IndicatorPoint.swift | 6 +- .../SwiftUICharts/Charts/LineChart/Line.swift | 4 +- .../{ => Charts}/PieChart/PieChart.swift | 0 .../{ => Charts}/PieChart/PieChartCell.swift | 0 .../{ => Charts}/PieChart/PieChartRow.swift | 0 .../ArrayExtensionTests.swift | 1 - .../SwiftUIChartsTests.swift | 2 +- .../SwiftUIChartsTests/XCTestManifests.swift | 2 +- 14 files changed, 65 insertions(+), 61 deletions(-) rename Sources/SwiftUICharts/{ => Charts}/PieChart/PieChart.swift (100%) rename Sources/SwiftUICharts/{ => Charts}/PieChart/PieChartCell.swift (100%) rename Sources/SwiftUICharts/{ => Charts}/PieChart/PieChartRow.swift (100%) diff --git a/Sources/SwiftUICharts/Base/Chart/ChartType.swift b/Sources/SwiftUICharts/Base/Chart/ChartType.swift index 7951d29d..b7bada4c 100644 --- a/Sources/SwiftUICharts/Base/Chart/ChartType.swift +++ b/Sources/SwiftUICharts/Base/Chart/ChartType.swift @@ -2,7 +2,7 @@ import SwiftUI @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) public protocol ChartType { - associatedtype Body : View + associatedtype Body: View func makeChart(configuration: Self.Configuration, style: Self.Style) -> Self.Body diff --git a/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift index f8241413..07f3325c 100644 --- a/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift @@ -23,7 +23,7 @@ extension CGPoint { return .zero } if let min = min, let max = max, min != max { - if (min <= 0) { + if min <= 0 { stepHeight = (frame.size.height - padding) / CGFloat(max - min) } else { stepHeight = (frame.size.height - padding) / CGFloat(max + min) diff --git a/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift b/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift index f1828ff7..9e9e3cdf 100644 --- a/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift +++ b/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift @@ -108,76 +108,80 @@ extension Path { return ret } - static func quadCurvedPathWithPoints(points:[Double], step:CGPoint, globalOffset: Double? = nil) -> Path { + static func quadCurvedPathWithPoints(points: [Double], step: CGPoint, globalOffset: Double? = nil) -> Path { var path = Path() - if (points.count < 2){ + if points.count < 2 { return path } let offset = globalOffset ?? points.min()! // guard let offset = points.min() else { return path } - var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y) - path.move(to: p1) + var point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y) + path.move(to: point1) for pointIndex in 1.. Path { + static func quadClosedCurvedPathWithPoints(points: [Double], step: CGPoint, globalOffset: Double? = nil) -> Path { var path = Path() - if (points.count < 2){ + if points.count < 2 { return path } let offset = globalOffset ?? points.min()! // guard let offset = points.min() else { return path } path.move(to: .zero) - var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y) - path.addLine(to: p1) + var point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y) + path.addLine(to: point1) for pointIndex in 1.. Path { + static func linePathWithPoints(points: [Double], step: CGPoint) -> Path { var path = Path() - if (points.count < 2){ + if points.count < 2 { return path } - guard let offset = points.min() else { return path } - let p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y) - path.move(to: p1) + guard let offset = points.min() else { + return path + } + let point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y) + path.move(to: point1) for pointIndex in 1.. Path { + static func closedLinePathWithPoints(points: [Double], step: CGPoint) -> Path { var path = Path() - if (points.count < 2){ + if points.count < 2 { + return path + } + guard let offset = points.min() else { return path } - guard let offset = points.min() else { return path } - var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y) - path.move(to: p1) + var point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y) + path.move(to: point1) for pointIndex in 1.. CGPoint { - let x = CGPoint.value(x: self.x, y: to.x, t: t, c1: control1.x, c2: control2.x) - let y = CGPoint.value(x: self.y, y: to.y, t: t, c1: control1.y, c2: control2.x) + let x = CGPoint.value(x: self.x, y: to.x, t: t, control1: control1.x, control2: control2.x) + let y = CGPoint.value(x: self.y, y: to.y, t: t, control1: control1.y, control2: control2.x) return CGPoint(x: x, y: y) } @@ -306,12 +310,12 @@ extension CGPoint { return value } - static func value(x: CGFloat, y: CGFloat, t: CGFloat, c1: CGFloat, c2: CGFloat) -> CGFloat { + static func value(x: CGFloat, y: CGFloat, t: CGFloat, control1: CGFloat, control2: CGFloat) -> CGFloat { var value: CGFloat = 0.0 // (1-t)^3 * p0 + 3 * (1-t)^2 * t * c1 + 3 * (1-t) * t^2 * c2 + t^3 * p1 value += pow(1-t, 3) * x - value += 3 * pow(1-t, 2) * t * c1 - value += 3 * (1-t) * pow(t, 2) * c2 + value += 3 * pow(1-t, 2) * t * control1 + value += 3 * (1-t) * pow(t, 2) * control2 value += pow(t, 3) * y return value } @@ -327,20 +331,21 @@ extension CGPoint { return sqrt((pow(self.x - to.x, 2) + pow(self.y - to.y, 2))) } - static func midPointForPoints(p1:CGPoint, p2:CGPoint) -> CGPoint { - return CGPoint(x:(p1.x + p2.x) / 2,y: (p1.y + p2.y) / 2) + static func midPointForPoints(firstPoint: CGPoint, secondPoint: CGPoint) -> CGPoint { + return CGPoint( + x: (firstPoint.x + secondPoint.x) / 2, + y: (firstPoint.y + secondPoint.y) / 2) } - static func controlPointForPoints(p1:CGPoint, p2:CGPoint) -> CGPoint { - var controlPoint = CGPoint.midPointForPoints(p1:p1, p2:p2) - let diffY = abs(p2.y - controlPoint.y) + static func controlPointForPoints(firstPoint: CGPoint, secondPoint: CGPoint) -> CGPoint { + var controlPoint = CGPoint.midPointForPoints(firstPoint: firstPoint, secondPoint: secondPoint) + let diffY = abs(secondPoint.y - controlPoint.y) - if (p1.y < p2.y){ + if firstPoint.y < secondPoint.y { controlPoint.y += diffY - } else if (p1.y > p2.y) { + } else if firstPoint.y > secondPoint.y { controlPoint.y -= diffY } return controlPoint } } - diff --git a/Sources/SwiftUICharts/Base/Extensions/View+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/View+Extension.swift index fbfc7fb2..06e57edc 100644 --- a/Sources/SwiftUICharts/Base/Extensions/View+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/View+Extension.swift @@ -1,7 +1,7 @@ import SwiftUI extension View { - public func type(_ type: S) -> some View where S : ChartType { + public func type(_ type: S) -> some View where S: ChartType { self.environment(\.chartType, AnyChartType(type)) } diff --git a/Sources/SwiftUICharts/Base/Label/ChartLabel.swift b/Sources/SwiftUICharts/Base/Label/ChartLabel.swift index a3134e54..6e304785 100644 --- a/Sources/SwiftUICharts/Base/Label/ChartLabel.swift +++ b/Sources/SwiftUICharts/Base/Label/ChartLabel.swift @@ -3,7 +3,7 @@ import SwiftUI @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) public protocol ChartLabel { - associatedtype Body : View + associatedtype Body: View func makeLabel(configuration: Self.Configuration) -> Self.Body diff --git a/Sources/SwiftUICharts/Base/Style/Colors.swift b/Sources/SwiftUICharts/Base/Style/Colors.swift index 9e3478be..0d166936 100644 --- a/Sources/SwiftUICharts/Base/Style/Colors.swift +++ b/Sources/SwiftUICharts/Base/Style/Colors.swift @@ -5,6 +5,6 @@ public enum ChartColors { static let orangeBright = Color(hexString: "#FF782C") static let orangeDark = Color(hexString: "#EC2301") - static let LegendColor:Color = Color(hexString: "#E8E7EA") - static let IndicatorKnob:Color = Color(hexString: "#FF57A6") + static let legendColor: Color = Color(hexString: "#E8E7EA") + static let indicatorKnob: Color = Color(hexString: "#FF57A6") } diff --git a/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift b/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift index 5a3f5694..925ecc7f 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift @@ -10,14 +10,14 @@ import SwiftUI struct IndicatorPoint: View { var body: some View { - ZStack{ + ZStack { Circle() - .fill(ChartColors.IndicatorKnob) + .fill(ChartColors.indicatorKnob) Circle() .stroke(Color.white, style: StrokeStyle(lineWidth: 4)) } .frame(width: 14, height: 14) - .shadow(color: ChartColors.LegendColor, radius: 6, x: 0, y: 6) + .shadow(color: ChartColors.legendColor, radius: 6, x: 0, y: 6) } } diff --git a/Sources/SwiftUICharts/Charts/LineChart/Line.swift b/Sources/SwiftUICharts/Charts/LineChart/Line.swift index 2138d5a8..ce51c420 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/Line.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/Line.swift @@ -45,14 +45,14 @@ public struct Line: View { self.getBackgroundPathView() } self.getLinePathView() - if(self.showIndicator) { + if self.showIndicator { IndicatorPoint() .position(self.getClosestPointOnPath(touchLocation: self.touchLocation)) .rotationEffect(.degrees(180), anchor: .center) .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) } } - .onAppear() { + .onAppear { self.frame = geometry.frame(in: .local) } .gesture(DragGesture() diff --git a/Sources/SwiftUICharts/PieChart/PieChart.swift b/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift similarity index 100% rename from Sources/SwiftUICharts/PieChart/PieChart.swift rename to Sources/SwiftUICharts/Charts/PieChart/PieChart.swift diff --git a/Sources/SwiftUICharts/PieChart/PieChartCell.swift b/Sources/SwiftUICharts/Charts/PieChart/PieChartCell.swift similarity index 100% rename from Sources/SwiftUICharts/PieChart/PieChartCell.swift rename to Sources/SwiftUICharts/Charts/PieChart/PieChartCell.swift diff --git a/Sources/SwiftUICharts/PieChart/PieChartRow.swift b/Sources/SwiftUICharts/Charts/PieChart/PieChartRow.swift similarity index 100% rename from Sources/SwiftUICharts/PieChart/PieChartRow.swift rename to Sources/SwiftUICharts/Charts/PieChart/PieChartRow.swift diff --git a/Tests/SwiftUIChartsTests/ArrayExtensionTests.swift b/Tests/SwiftUIChartsTests/ArrayExtensionTests.swift index 35b1e304..ad77114c 100644 --- a/Tests/SwiftUIChartsTests/ArrayExtensionTests.swift +++ b/Tests/SwiftUIChartsTests/ArrayExtensionTests.swift @@ -42,4 +42,3 @@ class ArrayExtensionTests: XCTestCase { } } - diff --git a/Tests/SwiftUIChartsTests/SwiftUIChartsTests.swift b/Tests/SwiftUIChartsTests/SwiftUIChartsTests.swift index 0171d836..dc7dc53a 100644 --- a/Tests/SwiftUIChartsTests/SwiftUIChartsTests.swift +++ b/Tests/SwiftUIChartsTests/SwiftUIChartsTests.swift @@ -9,6 +9,6 @@ final class SwiftUIChartsTests: XCTestCase { } static var allTests = [ - ("testExample", testExample), + ("testExample", testExample) ] } diff --git a/Tests/SwiftUIChartsTests/XCTestManifests.swift b/Tests/SwiftUIChartsTests/XCTestManifests.swift index a3999a87..e80e5857 100644 --- a/Tests/SwiftUIChartsTests/XCTestManifests.swift +++ b/Tests/SwiftUIChartsTests/XCTestManifests.swift @@ -3,7 +3,7 @@ import XCTest #if !canImport(ObjectiveC) public func allTests() -> [XCTestCaseEntry] { return [ - testCase(SwiftUIChartsTests.allTests), + testCase(SwiftUIChartsTests.allTests) ] } #endif From 0caebce9ff8d92d663299f2c28338ce5fbfad126 Mon Sep 17 00:00:00 2001 From: Andras Samu Date: Thu, 28 May 2020 18:50:59 +0200 Subject: [PATCH 09/29] added mac os as a build target --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 5a466752..3339018c 100644 --- a/Package.swift +++ b/Package.swift @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "SwiftUICharts", platforms: [ - .iOS(.v13), .watchOS(.v6) + .iOS(.v13), .watchOS(.v6), .macOS(.v10_15) ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. From d64d0e9d7ad863b9490b16027ff6d0fc3bb3fbbc Mon Sep 17 00:00:00 2001 From: nicolas Date: Sat, 30 May 2020 11:18:01 -0400 Subject: [PATCH 10/29] Bug Fix: Bar Chart with [0] crashed (#110) --- .../SwiftUICharts/Charts/BarChart/BarChart.swift | 3 +++ .../Charts/BarChart/BarChartCell.swift | 1 + .../SwiftUICharts/Charts/BarChart/BarChartRow.swift | 6 +++++- .../SwiftUICharts/Charts/LineChart/LineChart.swift | 3 +++ .../SwiftUICharts/Charts/PieChart/PieChart.swift | 4 ++++ .../Charts/PieChart/PieChartCell.swift | 13 +++++++++++-- .../SwiftUICharts/Charts/PieChart/PieChartRow.swift | 13 +++++++++---- 7 files changed, 36 insertions(+), 7 deletions(-) diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift index d91d1db0..e655683d 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift @@ -10,6 +10,9 @@ public struct BarChart: ChartType { struct BarChart_Previews: PreviewProvider { static var previews: some View { Group { + BarChart().makeChart( + configuration: .init(data: [0]), + style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack)) Group { BarChart().makeChart( configuration: .init(data: [1, 2, 3, 5, 1]), diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift index 22f2301b..0ab981a9 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift @@ -31,6 +31,7 @@ public struct BarChartCell: View { struct BarChartCell_Previews: PreviewProvider { static var previews: some View { Group { + BarChartCell(value: 0, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: .constant(CGFloat())) Group { BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: .constant(CGFloat())) BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: .constant(CGFloat())) diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift index 4a517597..5f423e26 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift @@ -11,7 +11,10 @@ public struct BarChartRow: View { var style: ChartStyle var maxValue: Double { - data.max() ?? 0 + guard let max = data.max() else { + return 1 + } + return max != 0 ? max : 1 } public var body: some View { @@ -59,6 +62,7 @@ public struct BarChartRow: View { struct BarChartRow_Previews: PreviewProvider { static var previews: some View { Group { + BarChartRow(data: [0], style: styleGreenRed) Group { BarChartRow(data: [1, 2, 3], style: styleGreenRed) BarChartRow(data: [1, 2, 3], style: styleGreenRedWhiteBlack) diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift b/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift index 5bd76709..2917b4c8 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift @@ -11,6 +11,9 @@ public struct LineChart: ChartType { struct LineChart_Previews: PreviewProvider { static var previews: some View { Group { + LineChart().makeChart( + configuration: .init(data: [0]), + style: .init(backgroundColor: .white, foregroundColor: ColorGradient(.black))) Group { LineChart().makeChart( configuration: .init(data: [1, 2, 3, 5, 1]), diff --git a/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift b/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift index 9794fda8..721f8ce3 100644 --- a/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift +++ b/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift @@ -17,6 +17,10 @@ public struct PieChart: ChartType { struct PieChart_Previews: PreviewProvider { static var previews: some View { Group { + PieChart().makeChart( + configuration: .init(data: [0]), + style: styleOneColor) + Group { PieChart().makeChart( configuration: .init(data: [56, 78, 53, 65, 54]), diff --git a/Sources/SwiftUICharts/Charts/PieChart/PieChartCell.swift b/Sources/SwiftUICharts/Charts/PieChart/PieChartCell.swift index 20157677..7dedd730 100644 --- a/Sources/SwiftUICharts/Charts/PieChart/PieChartCell.swift +++ b/Sources/SwiftUICharts/Charts/PieChart/PieChartCell.swift @@ -12,7 +12,6 @@ struct PieSlice: Identifiable { var startDeg: Double var endDeg: Double var value: Double - //var normalizedValue: Double } public struct PieChartCell: View { @@ -47,7 +46,7 @@ public struct PieChartCell: View { Group { path .fill(self.accentColor.linearGradient(from: .bottom, to: .top)) - .overlay(path.stroke(self.backgroundColor, lineWidth: 2)) + .overlay(path.stroke(self.backgroundColor, lineWidth: (startDeg == 0 && endDeg == 0 ? 0 : 2))) .scaleEffect(self.show ? 1 : 0) .animation(Animation.spring().delay(Double(self.index) * 0.04)) .onAppear { @@ -97,6 +96,16 @@ struct PieChartCell_Previews: PreviewProvider { rect: geometry.frame(in: .local), startDeg: 185.0, endDeg: 290.0, + index: 1, + backgroundColor: Color.purple, + accentColor: ColorGradient(.purple)) + }.frame(width: 100, height: 100) + + GeometryReader { geometry in + PieChartCell( + rect: geometry.frame(in: .local), + startDeg: 0, + endDeg: 0, index: 0, backgroundColor: Color.purple, accentColor: ColorGradient(.purple)) diff --git a/Sources/SwiftUICharts/Charts/PieChart/PieChartRow.swift b/Sources/SwiftUICharts/Charts/PieChart/PieChartRow.swift index f9428179..de76cd68 100644 --- a/Sources/SwiftUICharts/Charts/PieChart/PieChartRow.swift +++ b/Sources/SwiftUICharts/Charts/PieChart/PieChartRow.swift @@ -15,10 +15,10 @@ public struct PieChartRow: View { var slices: [PieSlice] { var tempSlices: [PieSlice] = [] var lastEndDeg: Double = 0 - let maxValue = data.reduce(0, +) + let maxValue: Double = data.reduce(0, +) for slice in data { - let normalized: Double = Double(slice)/Double(maxValue) + let normalized: Double = Double(slice) / (maxValue == 0 ? 1 : maxValue) let startDeg = lastEndDeg let endDeg = lastEndDeg + (normalized * 360) lastEndDeg = endDeg @@ -55,12 +55,17 @@ struct PieChartRow_Previews: PreviewProvider { PieChartRow( data: [8, 23, 32, 7, 23, 43], style: defaultMultiColorChartStyle) - .frame(width: 100, height: 100) + .frame(width: 100, height: 100) PieChartRow( data: [8, 23, 32, 7, 23, 43], style: multiColorChartStyle) - . frame(width: 100, height: 100) + .frame(width: 100, height: 100) + + PieChartRow( + data: [0], + style: multiColorChartStyle) + .frame(width: 100, height: 100) }.previewLayout(.fixed(width: 125, height: 125)) From f0eea58bd8e54a1771d740b98d008892bba3284f Mon Sep 17 00:00:00 2001 From: Andras Samu Date: Sat, 30 May 2020 18:07:49 +0200 Subject: [PATCH 11/29] Add CardView and CardLabel (#111) --- .../Base/CardView/CardView.swift | 23 ++++++++++ Sources/SwiftUICharts/Base/ChartView.swift | 1 - .../Base/Label/AnyChartLabel.swift | 19 --------- .../SwiftUICharts/Base/Label/ChartLabel.swift | 42 ++++++++++++++++--- .../Base/Label/ChartLabelConfiguration.swift | 7 ---- .../Charts/BarChart/TitleLabel.swift | 7 ---- .../Environment/Environment.swift | 9 ---- .../Environment/EnvironmentKeys.swift | 4 -- 8 files changed, 60 insertions(+), 52 deletions(-) create mode 100644 Sources/SwiftUICharts/Base/CardView/CardView.swift delete mode 100644 Sources/SwiftUICharts/Base/Label/AnyChartLabel.swift delete mode 100644 Sources/SwiftUICharts/Base/Label/ChartLabelConfiguration.swift delete mode 100644 Sources/SwiftUICharts/Charts/BarChart/TitleLabel.swift diff --git a/Sources/SwiftUICharts/Base/CardView/CardView.swift b/Sources/SwiftUICharts/Base/CardView/CardView.swift new file mode 100644 index 00000000..ecfe8f3e --- /dev/null +++ b/Sources/SwiftUICharts/Base/CardView/CardView.swift @@ -0,0 +1,23 @@ +import SwiftUI + +public struct CardView: View { + @Environment(\.chartStyle) private var chartStyle + + let content: () -> Content + + public init(@ViewBuilder content: @escaping () -> Content) { + self.content = content + } + + public var body: some View { + ZStack{ + Rectangle() + .fill(self.chartStyle.backgroundColor.linearGradient(from: .bottom, to: .top)) + .cornerRadius(20) + .shadow(color: Color.gray, radius: 8) + VStack { + self.content() + } + } + } +} diff --git a/Sources/SwiftUICharts/Base/ChartView.swift b/Sources/SwiftUICharts/Base/ChartView.swift index 9a65c87d..5958a711 100644 --- a/Sources/SwiftUICharts/Base/ChartView.swift +++ b/Sources/SwiftUICharts/Base/ChartView.swift @@ -6,7 +6,6 @@ import SwiftUI public struct ChartView: View { @Environment(\.chartType) private var chartType @Environment(\.chartStyle) private var chartStyle - @Environment(\.title) private var title private var configuration: ChartTypeConfiguration diff --git a/Sources/SwiftUICharts/Base/Label/AnyChartLabel.swift b/Sources/SwiftUICharts/Base/Label/AnyChartLabel.swift deleted file mode 100644 index 7aaee0a0..00000000 --- a/Sources/SwiftUICharts/Base/Label/AnyChartLabel.swift +++ /dev/null @@ -1,19 +0,0 @@ -import SwiftUI - -struct AnyChartLabel: ChartLabel { - private let labelMaker: (ChartLabel.Configuration) -> AnyView - - init(_ label: S) { - self.labelMaker = label.makeTypeErasedBody - } - - func makeLabel(configuration: ChartLabel.Configuration) -> AnyView { - self.labelMaker(configuration) - } -} - -fileprivate extension ChartLabel { - func makeTypeErasedBody(configuration: ChartLabel.Configuration) -> AnyView { - AnyView(makeLabel(configuration: configuration)) - } -} diff --git a/Sources/SwiftUICharts/Base/Label/ChartLabel.swift b/Sources/SwiftUICharts/Base/Label/ChartLabel.swift index 6e304785..95301dba 100644 --- a/Sources/SwiftUICharts/Base/Label/ChartLabel.swift +++ b/Sources/SwiftUICharts/Base/Label/ChartLabel.swift @@ -1,11 +1,43 @@ import SwiftUI -@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) -public protocol ChartLabel { +public enum ChartLabelSize: CGFloat { + case small = 16.0 + case normal = 24.0 + case large = 32.0 +} + +public enum ChartLabelType { + case title + case legend +} + +public struct ChartLabel: View { + private let text: String + private let labelSize: ChartLabelSize + private let labelType: ChartLabelType - associatedtype Body: View + private var labelColor: Color { + switch labelType { + case .title: + return .black + case .legend: + return .gray + } + } - func makeLabel(configuration: Self.Configuration) -> Self.Body + public init (_ text: String, + type: ChartLabelType = .title, + size: ChartLabelSize = .normal) { + self.text = text + labelType = type + labelSize = size + } - typealias Configuration = ChartLabelConfiguration + public var body: some View { + Text(self.text) + .font(.system(size: labelSize.rawValue)) + .bold() + .foregroundColor(self.labelColor) + .padding([.top, .bottom], 16.0) + } } diff --git a/Sources/SwiftUICharts/Base/Label/ChartLabelConfiguration.swift b/Sources/SwiftUICharts/Base/Label/ChartLabelConfiguration.swift deleted file mode 100644 index 846126e8..00000000 --- a/Sources/SwiftUICharts/Base/Label/ChartLabelConfiguration.swift +++ /dev/null @@ -1,7 +0,0 @@ -import SwiftUI - -public struct ChartLabelConfiguration { - public let font: Font - public let size: CGFloat - public let color: Color -} diff --git a/Sources/SwiftUICharts/Charts/BarChart/TitleLabel.swift b/Sources/SwiftUICharts/Charts/BarChart/TitleLabel.swift deleted file mode 100644 index c1962199..00000000 --- a/Sources/SwiftUICharts/Charts/BarChart/TitleLabel.swift +++ /dev/null @@ -1,7 +0,0 @@ -import SwiftUI - -public struct TitleLabel: ChartLabel { - public func makeLabel(configuration: Self.Configuration) -> some View { - return Text("AAA") - } -} diff --git a/Sources/SwiftUICharts/Environment/Environment.swift b/Sources/SwiftUICharts/Environment/Environment.swift index b90e9383..9ebe82ad 100644 --- a/Sources/SwiftUICharts/Environment/Environment.swift +++ b/Sources/SwiftUICharts/Environment/Environment.swift @@ -18,13 +18,4 @@ extension EnvironmentValues { self[ChartStyleKey.self] = newValue } } - - var title: AnyChartLabel { - get { - return self[ChartLabelKey.self] - } - set { - self[ChartLabelKey.self] = newValue - } - } } diff --git a/Sources/SwiftUICharts/Environment/EnvironmentKeys.swift b/Sources/SwiftUICharts/Environment/EnvironmentKeys.swift index f67f48e3..88950abc 100644 --- a/Sources/SwiftUICharts/Environment/EnvironmentKeys.swift +++ b/Sources/SwiftUICharts/Environment/EnvironmentKeys.swift @@ -9,7 +9,3 @@ struct ChartStyleKey: EnvironmentKey { foregroundColor: ColorGradient(ChartColors.orangeDark, ChartColors.orangeBright)) } - -struct ChartLabelKey: EnvironmentKey { - static let defaultValue: AnyChartLabel = AnyChartLabel(TitleLabel()) -} From dff16e8d2d1c90cbc95e46f23fcd41139f52c80e Mon Sep 17 00:00:00 2001 From: Andras Samu Date: Sun, 21 Jun 2020 18:53:11 +0200 Subject: [PATCH 12/29] Creating a data structure which propagets changes in data to the charts (#114) * Creating a data structure which propagets changes in data to the charts * Fixed appearing animation --- .../Base/Chart/AnyChartType.swift | 10 +-- .../SwiftUICharts/Base/Chart/ChartData.swift | 9 +++ .../SwiftUICharts/Base/Chart/ChartType.swift | 4 +- .../Base/Chart/ChartTypeConfiguration.swift | 5 -- Sources/SwiftUICharts/Base/ChartView.swift | 16 ++-- .../Charts/BarChart/BarChart.swift | 10 +-- .../Charts/BarChart/BarChartCell.swift | 64 +++++++++------ .../Charts/BarChart/BarChartRow.swift | 77 ++++++++++--------- .../SwiftUICharts/Charts/LineChart/Line.swift | 13 ++-- .../Charts/LineChart/LineChart.swift | 10 +-- .../Charts/PieChart/PieChart.swift | 18 ++--- .../Charts/PieChart/PieChartRow.swift | 14 ++-- 12 files changed, 140 insertions(+), 110 deletions(-) create mode 100644 Sources/SwiftUICharts/Base/Chart/ChartData.swift delete mode 100644 Sources/SwiftUICharts/Base/Chart/ChartTypeConfiguration.swift diff --git a/Sources/SwiftUICharts/Base/Chart/AnyChartType.swift b/Sources/SwiftUICharts/Base/Chart/AnyChartType.swift index 046dd164..7a758fa4 100644 --- a/Sources/SwiftUICharts/Base/Chart/AnyChartType.swift +++ b/Sources/SwiftUICharts/Base/Chart/AnyChartType.swift @@ -1,19 +1,19 @@ import SwiftUI struct AnyChartType: ChartType { - private let chartMaker: (ChartType.Configuration, ChartType.Style) -> AnyView + private let chartMaker: (ChartType.Data, ChartType.Style) -> AnyView init(_ type: S) { self.chartMaker = type.makeTypeErasedBody } - func makeChart(configuration: ChartType.Configuration, style: ChartType.Style) -> AnyView { - self.chartMaker(configuration, style) + func makeChart(data: ChartType.Data, style: ChartType.Style) -> AnyView { + self.chartMaker(data, style) } } fileprivate extension ChartType { - func makeTypeErasedBody(configuration: ChartType.Configuration, style: ChartType.Style) -> AnyView { - AnyView(makeChart(configuration: configuration, style: style)) + func makeTypeErasedBody(data: ChartType.Data, style: ChartType.Style) -> AnyView { + AnyView(makeChart(data: data, style: style)) } } diff --git a/Sources/SwiftUICharts/Base/Chart/ChartData.swift b/Sources/SwiftUICharts/Base/Chart/ChartData.swift new file mode 100644 index 00000000..9d546de4 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Chart/ChartData.swift @@ -0,0 +1,9 @@ +import SwiftUI + +public class ChartData: ObservableObject { + @Published public var data: [Double] = [] + + public init(_ data: [Double]) { + self.data = data + } +} diff --git a/Sources/SwiftUICharts/Base/Chart/ChartType.swift b/Sources/SwiftUICharts/Base/Chart/ChartType.swift index b7bada4c..de1dc964 100644 --- a/Sources/SwiftUICharts/Base/Chart/ChartType.swift +++ b/Sources/SwiftUICharts/Base/Chart/ChartType.swift @@ -4,8 +4,8 @@ import SwiftUI public protocol ChartType { associatedtype Body: View - func makeChart(configuration: Self.Configuration, style: Self.Style) -> Self.Body + func makeChart(data: Self.Data, style: Self.Style) -> Self.Body - typealias Configuration = ChartTypeConfiguration + typealias Data = ChartData typealias Style = ChartStyle } diff --git a/Sources/SwiftUICharts/Base/Chart/ChartTypeConfiguration.swift b/Sources/SwiftUICharts/Base/Chart/ChartTypeConfiguration.swift deleted file mode 100644 index 0e56a54c..00000000 --- a/Sources/SwiftUICharts/Base/Chart/ChartTypeConfiguration.swift +++ /dev/null @@ -1,5 +0,0 @@ -import SwiftUI - -public struct ChartTypeConfiguration { - public let data: [Double] -} diff --git a/Sources/SwiftUICharts/Base/ChartView.swift b/Sources/SwiftUICharts/Base/ChartView.swift index 5958a711..d13aeba2 100644 --- a/Sources/SwiftUICharts/Base/ChartView.swift +++ b/Sources/SwiftUICharts/Base/ChartView.swift @@ -7,15 +7,21 @@ public struct ChartView: View { @Environment(\.chartType) private var chartType @Environment(\.chartStyle) private var chartStyle - private var configuration: ChartTypeConfiguration + private var data: ChartData + + public init(data: ChartData) { + self.data = data + } public var body: some View { - self.chartType.makeChart(configuration: configuration, style: chartStyle) + self.chartType.makeChart(data: data, style: chartStyle) } } extension ChartView { - public init(data: [Double]) { - self.configuration = ChartTypeConfiguration(data: data) - } +// public init(data: [Double]) { +// self.configuration = ChartTypeConfiguration(data: data) +// } + + } diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift index e655683d..a5e5c73e 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift @@ -1,8 +1,8 @@ import SwiftUI public struct BarChart: ChartType { - public func makeChart(configuration: Self.Configuration, style: Self.Style) -> some View { - BarChartRow(data: configuration.data, style: style) + public func makeChart(data: Self.Data, style: Self.Style) -> some View { + BarChartRow(chartData: data, style: style) } public init() {} } @@ -11,17 +11,17 @@ struct BarChart_Previews: PreviewProvider { static var previews: some View { Group { BarChart().makeChart( - configuration: .init(data: [0]), + data: .init([0]), style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack)) Group { BarChart().makeChart( - configuration: .init(data: [1, 2, 3, 5, 1]), + data: .init([1, 2, 3, 5, 1]), style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack)) }.environment(\.colorScheme, .light) Group { BarChart().makeChart( - configuration: .init(data: [1, 2, 3]), + data: .init([1, 2, 3]), style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack)) }.environment(\.colorScheme, .dark) diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift index 0ab981a9..16936138 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift @@ -1,49 +1,67 @@ import SwiftUI public struct BarChartCell: View { - @State var value: Double - @State var index: Int = 0 - @State var width: Float - @State var numberOfDataPoints: Int + var value: Double + var index: Int = 0 + var width: Float + var numberOfDataPoints: Int var gradientColor: ColorGradient + var touchLocation: CGFloat var cellWidth: Double { return Double(width)/(Double(numberOfDataPoints) * 1.5) } - @State var scaleValue: Double = 0 - @Binding var touchLocation: CGFloat - + @State var firstDisplay: Bool = true + + public init( value: Double, + index: Int = 0, + width: Float, + numberOfDataPoints: Int, + gradientColor: ColorGradient, + touchLocation: CGFloat) { + self.value = value + self.index = index + self.width = width + self.numberOfDataPoints = numberOfDataPoints + self.gradientColor = gradientColor + self.touchLocation = touchLocation + } + public var body: some View { ZStack { RoundedRectangle(cornerRadius: 4) .fill(gradientColor.linearGradient(from: .bottom, to: .top)) - } - .frame(width: CGFloat(self.cellWidth)) - .scaleEffect(CGSize(width: 1, height: self.scaleValue), anchor: .bottom) - .onAppear { - self.scaleValue = self.value - } - .animation(Animation.spring().delay(self.touchLocation < 0 ? Double(self.index) * 0.04 : 0)) + } + .frame(width: CGFloat(self.cellWidth)) + .scaleEffect(CGSize(width: 1, height: self.firstDisplay ? 0.0 : self.value), anchor: .bottom) + .onAppear { + self.firstDisplay = false + } + .onDisappear { + self.firstDisplay = true + } + .transition(.slide) + .animation(Animation.spring().delay(self.touchLocation < 0 || !firstDisplay ? Double(self.index) * 0.04 : 0)) } } struct BarChartCell_Previews: PreviewProvider { static var previews: some View { Group { - BarChartCell(value: 0, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: .constant(CGFloat())) Group { - BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: .constant(CGFloat())) - BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: .constant(CGFloat())) - BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient(.purple), touchLocation: .constant(CGFloat())) + BarChartCell(value: 0, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat()) + + BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat()) + BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat()) + BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat()) } - + Group { - BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: .constant(CGFloat())) - BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: .constant(CGFloat())) - BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient(.purple), touchLocation: .constant(CGFloat())) + BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat()) + BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat()) + BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat()) }.environment(\.colorScheme, .dark) } - } } diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift index 5f423e26..21e95af0 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift @@ -1,7 +1,7 @@ import SwiftUI public struct BarChartRow: View { - @State var data: [Double] = [] + @ObservedObject var chartData: ChartData @State var touchLocation: CGFloat = -1.0 enum Constant { @@ -11,7 +11,7 @@ public struct BarChartRow: View { var style: ChartStyle var maxValue: Double { - guard let max = data.max() else { + guard let max = chartData.data.max() else { return 1 } return max != 0 ? max : 1 @@ -20,18 +20,18 @@ public struct BarChartRow: View { public var body: some View { GeometryReader { geometry in HStack(alignment: .bottom, - spacing: (geometry.frame(in: .local).width - Constant.spacing) / CGFloat(self.data.count * 3)) { - ForEach(0.. Double { - return Double(data[index])/Double(maxValue) + print(chartData.data[index]) + return Double(chartData.data[index])/Double(maxValue) } func getScaleSize(touchLocation: CGFloat, index: Int) -> CGSize { - if touchLocation > CGFloat(index)/CGFloat(self.data.count) && - touchLocation < CGFloat(index+1)/CGFloat(self.data.count) { + if touchLocation > CGFloat(index)/CGFloat(chartData.data.count) && + touchLocation < CGFloat(index+1)/CGFloat(chartData.data.count) { return CGSize(width: 1.4, height: 1.1) } return CGSize(width: 1, height: 1) @@ -59,24 +60,24 @@ public struct BarChartRow: View { } -struct BarChartRow_Previews: PreviewProvider { - static var previews: some View { - Group { - BarChartRow(data: [0], style: styleGreenRed) - Group { - BarChartRow(data: [1, 2, 3], style: styleGreenRed) - BarChartRow(data: [1, 2, 3], style: styleGreenRedWhiteBlack) - } - Group { - BarChartRow(data: [1, 2, 3], style: styleGreenRed) - BarChartRow(data: [1, 2, 3], style: styleGreenRedWhiteBlack) - }.environment(\.colorScheme, .dark) - } - } -} - -private let styleGreenRed = ChartStyle(backgroundColor: .white, foregroundColor: .greenRed) - -private let styleGreenRedWhiteBlack = ChartStyle( - backgroundColor: ColorGradient.init(.white), - foregroundColor: [ColorGradient.redBlack, ColorGradient.whiteBlack]) +//struct BarChartRow_Previews: PreviewProvider { +// static var previews: some View { +// Group { +// BarChartRow(data: [0], style: styleGreenRed) +// Group { +// BarChartRow(data: [1, 2, 3], style: styleGreenRed) +// BarChartRow(data: [1, 2, 3], style: styleGreenRedWhiteBlack) +// } +// Group { +// BarChartRow(data: [1, 2, 3], style: styleGreenRed) +// BarChartRow(data: [1, 2, 3], style: styleGreenRedWhiteBlack) +// }.environment(\.colorScheme, .dark) +// } +// } +//} +// +//private let styleGreenRed = ChartStyle(backgroundColor: .white, foregroundColor: .greenRed) +// +//private let styleGreenRedWhiteBlack = ChartStyle( +// backgroundColor: ColorGradient.init(.white), +// foregroundColor: [ColorGradient.redBlack, ColorGradient.whiteBlack]) diff --git a/Sources/SwiftUICharts/Charts/LineChart/Line.swift b/Sources/SwiftUICharts/Charts/LineChart/Line.swift index ce51c420..7eaa484a 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/Line.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/Line.swift @@ -2,7 +2,8 @@ import SwiftUI public struct Line: View { @State var frame: CGRect = .zero - @State var data: [Double] + @ObservedObject var chartData: ChartData + var style: ChartStyle @State var showIndicator: Bool = false @@ -11,11 +12,11 @@ public struct Line: View { @State var showBackground: Bool = true var curvedLines: Bool = true var step: CGPoint { - return CGPoint.getStep(frame: frame, data: data) + return CGPoint.getStep(frame: frame, data: chartData.data) } var path: Path { - let points = data + let points = chartData.data if curvedLines { return Path.quadCurvedPathWithPoints(points: points, @@ -27,7 +28,7 @@ public struct Line: View { } var closedPath: Path { - let points = data + let points = chartData.data if curvedLines { return Path.quadClosedCurvedPathWithPoints(points: points, @@ -109,8 +110,8 @@ extension Line { struct Line_Previews: PreviewProvider { static var previews: some View { Group { - Line(data: [1, 2, 3, 1, 2, 5, 7], style: blackLineStyle) - Line(data: [1, 2, 3, 1, 2, 5, 7], style: redLineStyle) + Line(chartData: ChartData([8, 23, 32, 7, 23, 43]), style: blackLineStyle) + Line(chartData: ChartData([8, 23, 32, 7, 23, 43]), style: redLineStyle) } } } diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift b/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift index 2917b4c8..e86579a2 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift @@ -1,8 +1,8 @@ import SwiftUI public struct LineChart: ChartType { - public func makeChart(configuration: Self.Configuration, style: Self.Style) -> some View { - Line(data: configuration.data, style: style) + public func makeChart(data: Self.Data, style: Self.Style) -> some View { + Line(chartData: data, style: style) } public init() {} @@ -12,17 +12,17 @@ struct LineChart_Previews: PreviewProvider { static var previews: some View { Group { LineChart().makeChart( - configuration: .init(data: [0]), + data: .init([0]), style: .init(backgroundColor: .white, foregroundColor: ColorGradient(.black))) Group { LineChart().makeChart( - configuration: .init(data: [1, 2, 3, 5, 1]), + data: .init([1, 2, 3, 5, 1]), style: .init(backgroundColor: .white, foregroundColor: ColorGradient(.black))) }.environment(\.colorScheme, .light) Group { LineChart().makeChart( - configuration: .init(data: [1, 2, 3]), + data: .init([1, 2, 3]), style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack)) }.environment(\.colorScheme, .dark) diff --git a/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift b/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift index 721f8ce3..2907a191 100644 --- a/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift +++ b/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift @@ -8,8 +8,8 @@ import SwiftUI public struct PieChart: ChartType { - public func makeChart(configuration: Self.Configuration, style: Self.Style) -> some View { - PieChartRow(data: configuration.data, style: style) + public func makeChart(data: Self.Data, style: Self.Style) -> some View { + PieChartRow(chartData: data, style: style) } public init() {} } @@ -18,30 +18,30 @@ struct PieChart_Previews: PreviewProvider { static var previews: some View { Group { PieChart().makeChart( - configuration: .init(data: [0]), + data: .init([0]), style: styleOneColor) Group { PieChart().makeChart( - configuration: .init(data: [56, 78, 53, 65, 54]), + data: .init([56, 78, 53, 65, 54]), style: styleOneColor) PieChart().makeChart( - configuration: .init(data: [56, 78, 53, 65, 54]), + data: .init([56, 78, 53, 65, 54]), style: styleTwoColor) PieChart().makeChart( - configuration: .init(data: [1, 1, 1, 1, 1, 1]), + data: .init([1, 1, 1, 1, 1, 1]), style: trivialPursuit) }.environment(\.colorScheme, .light) Group { PieChart().makeChart( - configuration: .init(data: [56, 78, 53, 65, 54]), + data: .init([56, 78, 53, 65, 54]), style: styleOneColor) PieChart().makeChart( - configuration: .init(data: [56, 78, 53, 65, 54]), + data: .init([56, 78, 53, 65, 54]), style: styleTwoColor) PieChart().makeChart( - configuration: .init(data: [1, 1, 1, 1, 1, 1]), + data: .init([1, 1, 1, 1, 1, 1]), style: trivialPursuit) }.environment(\.colorScheme, .dark) diff --git a/Sources/SwiftUICharts/Charts/PieChart/PieChartRow.swift b/Sources/SwiftUICharts/Charts/PieChart/PieChartRow.swift index de76cd68..aae06735 100644 --- a/Sources/SwiftUICharts/Charts/PieChart/PieChartRow.swift +++ b/Sources/SwiftUICharts/Charts/PieChart/PieChartRow.swift @@ -8,16 +8,16 @@ import SwiftUI public struct PieChartRow: View { - var data: [Double] - + @ObservedObject var chartData: ChartData + var style: ChartStyle var slices: [PieSlice] { var tempSlices: [PieSlice] = [] var lastEndDeg: Double = 0 - let maxValue: Double = data.reduce(0, +) + let maxValue: Double = chartData.data.reduce(0, +) - for slice in data { + for slice in chartData.data { let normalized: Double = Double(slice) / (maxValue == 0 ? 1 : maxValue) let startDeg = lastEndDeg let endDeg = lastEndDeg + (normalized * 360) @@ -53,17 +53,17 @@ struct PieChartRow_Previews: PreviewProvider { Group { //Empty Array - Default Colors.OrangeStart PieChartRow( - data: [8, 23, 32, 7, 23, 43], + chartData: ChartData([8, 23, 32, 7, 23, 43]), style: defaultMultiColorChartStyle) .frame(width: 100, height: 100) PieChartRow( - data: [8, 23, 32, 7, 23, 43], + chartData: ChartData([8, 23, 32, 7, 23, 43]), style: multiColorChartStyle) .frame(width: 100, height: 100) PieChartRow( - data: [0], + chartData: ChartData([8, 23, 32, 7, 23, 43]), style: multiColorChartStyle) .frame(width: 100, height: 100) From 57ac969092925a55b4dd7c500e15ee52883f85c2 Mon Sep 17 00:00:00 2001 From: Andras Samu Date: Sun, 28 Jun 2020 20:25:21 +0200 Subject: [PATCH 13/29] Added ChartLabel interaction --- .../xcschemes/xcschememanagement.plist | 2 +- .../Base/CardView/CardView.swift | 3 +- .../SwiftUICharts/Base/Chart/ChartValue.swift | 6 ++ Sources/SwiftUICharts/Base/ChartView.swift | 11 +--- .../SwiftUICharts/Base/Grid/ChartGrid.swift | 16 +++++ .../SwiftUICharts/Base/Label/ChartLabel.swift | 65 ++++++++++++++----- .../Charts/BarChart/BarChart.swift | 1 + .../Charts/BarChart/BarChartRow.swift | 17 ++++- .../SwiftUICharts/Charts/LineChart/Line.swift | 11 ++++ .../Environment/Environment.swift | 9 +++ .../Environment/EnvironmentKeys.swift | 4 ++ 11 files changed, 114 insertions(+), 31 deletions(-) create mode 100644 Sources/SwiftUICharts/Base/Chart/ChartValue.swift create mode 100644 Sources/SwiftUICharts/Base/Grid/ChartGrid.swift diff --git a/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcschemes/xcschememanagement.plist b/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcschemes/xcschememanagement.plist index 540c36e2..a0f26bbb 100644 --- a/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ SwiftUICharts.xcscheme_^#shared#^_ orderHint - 1 + 2 diff --git a/Sources/SwiftUICharts/Base/CardView/CardView.swift b/Sources/SwiftUICharts/Base/CardView/CardView.swift index ecfe8f3e..87b20154 100644 --- a/Sources/SwiftUICharts/Base/CardView/CardView.swift +++ b/Sources/SwiftUICharts/Base/CardView/CardView.swift @@ -12,12 +12,13 @@ public struct CardView: View { public var body: some View { ZStack{ Rectangle() - .fill(self.chartStyle.backgroundColor.linearGradient(from: .bottom, to: .top)) + .fill(Color.white) .cornerRadius(20) .shadow(color: Color.gray, radius: 8) VStack { self.content() } + .clipShape(RoundedRectangle(cornerRadius: 20)) } } } diff --git a/Sources/SwiftUICharts/Base/Chart/ChartValue.swift b/Sources/SwiftUICharts/Base/Chart/ChartValue.swift new file mode 100644 index 00000000..368ae84e --- /dev/null +++ b/Sources/SwiftUICharts/Base/Chart/ChartValue.swift @@ -0,0 +1,6 @@ +import SwiftUI + +public class ChartValue: ObservableObject { + @Published var currentValue: Double = 0 + @Published var interactionInProgress: Bool = false +} diff --git a/Sources/SwiftUICharts/Base/ChartView.swift b/Sources/SwiftUICharts/Base/ChartView.swift index d13aeba2..09fea6d3 100644 --- a/Sources/SwiftUICharts/Base/ChartView.swift +++ b/Sources/SwiftUICharts/Base/ChartView.swift @@ -1,6 +1,3 @@ -// ChartView.swift -// Created by Samu András on 2020. 05. 22.. - import SwiftUI public struct ChartView: View { @@ -19,9 +16,7 @@ public struct ChartView: View { } extension ChartView { -// public init(data: [Double]) { -// self.configuration = ChartTypeConfiguration(data: data) -// } - - + public init(points: [Double]) { + self.data = ChartData(points) + } } diff --git a/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift b/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift new file mode 100644 index 00000000..6c5e273f --- /dev/null +++ b/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift @@ -0,0 +1,16 @@ +import SwiftUI + +public struct ChartGrid: View { + let content: () -> Content + + public init(@ViewBuilder content: @escaping () -> Content) { + self.content = content + } + + public var body: some View { + ZStack{ + self.content() + } + } +} + diff --git a/Sources/SwiftUICharts/Base/Label/ChartLabel.swift b/Sources/SwiftUICharts/Base/Label/ChartLabel.swift index 95301dba..03eba4d3 100644 --- a/Sources/SwiftUICharts/Base/Label/ChartLabel.swift +++ b/Sources/SwiftUICharts/Base/Label/ChartLabel.swift @@ -1,19 +1,36 @@ import SwiftUI -public enum ChartLabelSize: CGFloat { - case small = 16.0 - case normal = 24.0 - case large = 32.0 -} - public enum ChartLabelType { case title + case subTitle + case largeTitle + case custom(size: CGFloat) case legend } public struct ChartLabel: View { - private let text: String - private let labelSize: ChartLabelSize + @Environment(\.chartValue) private var chartValue: ChartValue + + @State var textToDisplay = "" + @State var isInteractionInProgress: Bool = false + + private var title: String + + private var labelSize: CGFloat { + switch labelType { + case .title: + return 32.0 + case .legend: + return 14.0 + case .subTitle: + return 24.0 + case .largeTitle: + return 38.0 + case .custom(let size): + return size + } + } + private let labelType: ChartLabelType private var labelColor: Color { @@ -22,22 +39,34 @@ public struct ChartLabel: View { return .black case .legend: return .gray + case .subTitle: + return .black + case .largeTitle: + return .black + case .custom(_): + return .black } } - public init (_ text: String, - type: ChartLabelType = .title, - size: ChartLabelSize = .normal) { - self.text = text + public init (_ title: String, + type: ChartLabelType = .title) { + self.title = title labelType = type - labelSize = size } public var body: some View { - Text(self.text) - .font(.system(size: labelSize.rawValue)) - .bold() - .foregroundColor(self.labelColor) - .padding([.top, .bottom], 16.0) + VStack (alignment: self.isInteractionInProgress ? .center : .leading) { + Text(textToDisplay) + .font(.system(size: labelSize)) + .bold() + .foregroundColor(self.labelColor) + .onAppear { + self.textToDisplay = title + } + .onReceive(self.chartValue.objectWillChange) { _ in + self.textToDisplay = self.chartValue.interactionInProgress ? String(format: "%.01f", self.chartValue.currentValue) : self.title + self.isInteractionInProgress = self.chartValue.interactionInProgress + } + } } } diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift index a5e5c73e..8edd0248 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift @@ -4,6 +4,7 @@ public struct BarChart: ChartType { public func makeChart(data: Self.Data, style: Self.Style) -> some View { BarChartRow(chartData: data, style: style) } + public init() {} } diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift index 21e95af0..f4f42865 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift @@ -1,6 +1,7 @@ import SwiftUI public struct BarChartRow: View { + @Environment(\.chartValue) private var chartValue: ChartValue @ObservedObject var chartData: ChartData @State var touchLocation: CGFloat = -1.0 @@ -36,9 +37,15 @@ public struct BarChartRow: View { .padding([.top, .leading, .trailing], 10) .gesture(DragGesture() .onChanged({ value in - self.touchLocation = value.location.x/geometry.frame(in: .local).width + let width = geometry.frame(in: .local).width + self.touchLocation = value.location.x/width + if let currentValue = getCurrentValue(width: width) { + self.chartValue.currentValue = currentValue + self.chartValue.interactionInProgress = true + } }) .onEnded({ value in + self.chartValue.interactionInProgress = false self.touchLocation = -1 }) ) @@ -46,7 +53,6 @@ public struct BarChartRow: View { } func normalizedValue(index: Int) -> Double { - print(chartData.data[index]) return Double(chartData.data[index])/Double(maxValue) } @@ -57,7 +63,12 @@ public struct BarChartRow: View { } return CGSize(width: 1, height: 1) } - + + func getCurrentValue(width: CGFloat) -> Double? { + guard self.chartData.data.count > 0 else { return nil} + let index = max(0,min(self.chartData.data.count-1,Int(floor((self.touchLocation*width)/(width/CGFloat(self.chartData.data.count)))))) + return self.chartData.data[index] + } } //struct BarChartRow_Previews: PreviewProvider { diff --git a/Sources/SwiftUICharts/Charts/LineChart/Line.swift b/Sources/SwiftUICharts/Charts/LineChart/Line.swift index 7eaa484a..c152255d 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/Line.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/Line.swift @@ -1,6 +1,7 @@ import SwiftUI public struct Line: View { + @Environment(\.chartValue) private var chartValue: ChartValue @State var frame: CGRect = .zero @ObservedObject var chartData: ChartData @@ -60,10 +61,13 @@ public struct Line: View { .onChanged({ value in self.touchLocation = value.location self.showIndicator = true + self.getClosestDataPoint(point: self.getClosestPointOnPath(touchLocation: value.location)) + self.chartValue.interactionInProgress = true }) .onEnded({ value in self.touchLocation = .zero self.showIndicator = false + self.chartValue.interactionInProgress = false }) ) } @@ -78,6 +82,13 @@ extension Line { return closest } + private func getClosestDataPoint(point: CGPoint) { + let index = Int(round((point.x)/step.x)) + if (index >= 0 && index < self.chartData.data.count){ + self.chartValue.currentValue = self.chartData.data[index] + } + } + private func getBackgroundPathView() -> some View { self.closedPath .fill(style.backgroundColor.linearGradient(from: .bottom, to: .top)) diff --git a/Sources/SwiftUICharts/Environment/Environment.swift b/Sources/SwiftUICharts/Environment/Environment.swift index 9ebe82ad..b7e0130d 100644 --- a/Sources/SwiftUICharts/Environment/Environment.swift +++ b/Sources/SwiftUICharts/Environment/Environment.swift @@ -18,4 +18,13 @@ extension EnvironmentValues { self[ChartStyleKey.self] = newValue } } + + var chartValue: ChartValue { + get { + return self[ChartValueKey.self] + } + set { + self[ChartValueKey.self] = newValue + } + } } diff --git a/Sources/SwiftUICharts/Environment/EnvironmentKeys.swift b/Sources/SwiftUICharts/Environment/EnvironmentKeys.swift index 88950abc..4cbcee81 100644 --- a/Sources/SwiftUICharts/Environment/EnvironmentKeys.swift +++ b/Sources/SwiftUICharts/Environment/EnvironmentKeys.swift @@ -9,3 +9,7 @@ struct ChartStyleKey: EnvironmentKey { foregroundColor: ColorGradient(ChartColors.orangeDark, ChartColors.orangeBright)) } + +struct ChartValueKey: EnvironmentKey { + static let defaultValue: ChartValue = ChartValue() +} From c46902dab8291705183b01fe3dab2c0d58aa18ef Mon Sep 17 00:00:00 2001 From: Andras Samu Date: Sat, 25 Jul 2020 18:56:58 +0200 Subject: [PATCH 14/29] Refactor Chart base (#143) --- .../Base/CardView/CardView.swift | 10 +-- .../Base/Chart/AnyChartType.swift | 19 ------ .../SwiftUICharts/Base/Chart/ChartBase.swift | 5 ++ .../SwiftUICharts/Base/Chart/ChartData.swift | 4 ++ .../SwiftUICharts/Base/Chart/ChartType.swift | 11 ---- Sources/SwiftUICharts/Base/ChartView.swift | 22 ------- .../Base/Extensions/Array+Extension.swift | 7 -- .../Base/Extensions/CGRect+Extension.swift | 7 -- .../Base/Extensions/ChartBase+Extension.swift | 10 +++ .../Base/Extensions/View+Extension.swift | 8 +-- .../SwiftUICharts/Base/Grid/ChartGrid.swift | 6 +- .../SwiftUICharts/Base/Label/ChartLabel.swift | 38 +++++++---- .../SwiftUICharts/Base/Style/ChartStyle.swift | 2 +- .../Charts/BarChart/BarChart.swift | 31 ++------- .../Charts/BarChart/BarChartRow.swift | 26 +------- .../SwiftUICharts/Charts/LineChart/Line.swift | 10 ++- .../Charts/LineChart/LineChart.swift | 34 +++------- .../Charts/PieChart/PieChart.swift | 65 +++---------------- .../Environment/Environment.swift | 30 --------- .../Environment/EnvironmentKeys.swift | 15 ----- 20 files changed, 92 insertions(+), 268 deletions(-) delete mode 100644 Sources/SwiftUICharts/Base/Chart/AnyChartType.swift create mode 100644 Sources/SwiftUICharts/Base/Chart/ChartBase.swift delete mode 100644 Sources/SwiftUICharts/Base/Chart/ChartType.swift delete mode 100644 Sources/SwiftUICharts/Base/ChartView.swift create mode 100644 Sources/SwiftUICharts/Base/Extensions/ChartBase+Extension.swift delete mode 100644 Sources/SwiftUICharts/Environment/Environment.swift delete mode 100644 Sources/SwiftUICharts/Environment/EnvironmentKeys.swift diff --git a/Sources/SwiftUICharts/Base/CardView/CardView.swift b/Sources/SwiftUICharts/Base/CardView/CardView.swift index 87b20154..08b65c86 100644 --- a/Sources/SwiftUICharts/Base/CardView/CardView.swift +++ b/Sources/SwiftUICharts/Base/CardView/CardView.swift @@ -1,19 +1,19 @@ import SwiftUI -public struct CardView: View { - @Environment(\.chartStyle) private var chartStyle - +public struct CardView: View, ChartBase { + public var chartData = ChartData() let content: () -> Content + @EnvironmentObject var style: ChartStyle + public init(@ViewBuilder content: @escaping () -> Content) { self.content = content } public var body: some View { ZStack{ - Rectangle() + RoundedRectangle(cornerRadius: 20) .fill(Color.white) - .cornerRadius(20) .shadow(color: Color.gray, radius: 8) VStack { self.content() diff --git a/Sources/SwiftUICharts/Base/Chart/AnyChartType.swift b/Sources/SwiftUICharts/Base/Chart/AnyChartType.swift deleted file mode 100644 index 7a758fa4..00000000 --- a/Sources/SwiftUICharts/Base/Chart/AnyChartType.swift +++ /dev/null @@ -1,19 +0,0 @@ -import SwiftUI - -struct AnyChartType: ChartType { - private let chartMaker: (ChartType.Data, ChartType.Style) -> AnyView - - init(_ type: S) { - self.chartMaker = type.makeTypeErasedBody - } - - func makeChart(data: ChartType.Data, style: ChartType.Style) -> AnyView { - self.chartMaker(data, style) - } -} - -fileprivate extension ChartType { - func makeTypeErasedBody(data: ChartType.Data, style: ChartType.Style) -> AnyView { - AnyView(makeChart(data: data, style: style)) - } -} diff --git a/Sources/SwiftUICharts/Base/Chart/ChartBase.swift b/Sources/SwiftUICharts/Base/Chart/ChartBase.swift new file mode 100644 index 00000000..3bc2f96a --- /dev/null +++ b/Sources/SwiftUICharts/Base/Chart/ChartBase.swift @@ -0,0 +1,5 @@ +import SwiftUI + +public protocol ChartBase { + var chartData: ChartData { get } +} diff --git a/Sources/SwiftUICharts/Base/Chart/ChartData.swift b/Sources/SwiftUICharts/Base/Chart/ChartData.swift index 9d546de4..b3afb828 100644 --- a/Sources/SwiftUICharts/Base/Chart/ChartData.swift +++ b/Sources/SwiftUICharts/Base/Chart/ChartData.swift @@ -6,4 +6,8 @@ public class ChartData: ObservableObject { public init(_ data: [Double]) { self.data = data } + + public init() { + self.data = [] + } } diff --git a/Sources/SwiftUICharts/Base/Chart/ChartType.swift b/Sources/SwiftUICharts/Base/Chart/ChartType.swift deleted file mode 100644 index de1dc964..00000000 --- a/Sources/SwiftUICharts/Base/Chart/ChartType.swift +++ /dev/null @@ -1,11 +0,0 @@ -import SwiftUI - -@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) -public protocol ChartType { - associatedtype Body: View - - func makeChart(data: Self.Data, style: Self.Style) -> Self.Body - - typealias Data = ChartData - typealias Style = ChartStyle -} diff --git a/Sources/SwiftUICharts/Base/ChartView.swift b/Sources/SwiftUICharts/Base/ChartView.swift deleted file mode 100644 index 09fea6d3..00000000 --- a/Sources/SwiftUICharts/Base/ChartView.swift +++ /dev/null @@ -1,22 +0,0 @@ -import SwiftUI - -public struct ChartView: View { - @Environment(\.chartType) private var chartType - @Environment(\.chartStyle) private var chartStyle - - private var data: ChartData - - public init(data: ChartData) { - self.data = data - } - - public var body: some View { - self.chartType.makeChart(data: data, style: chartStyle) - } -} - -extension ChartView { - public init(points: [Double]) { - self.data = ChartData(points) - } -} diff --git a/Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift index f2898316..1b2d837b 100644 --- a/Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift @@ -1,10 +1,3 @@ -// -// File.swift -// -// -// Created by Nicolas Savoini on 2020-05-25. -// - import Foundation extension Array where Element == ColorGradient { diff --git a/Sources/SwiftUICharts/Base/Extensions/CGRect+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/CGRect+Extension.swift index c84d8374..020717cc 100644 --- a/Sources/SwiftUICharts/Base/Extensions/CGRect+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/CGRect+Extension.swift @@ -1,10 +1,3 @@ -// -// CGRect+Extension.swift -// SwiftUICharts -// -// Created by Nicolas Savoini on 2020-05-24. -// - import Foundation import SwiftUI diff --git a/Sources/SwiftUICharts/Base/Extensions/ChartBase+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/ChartBase+Extension.swift new file mode 100644 index 00000000..b1c8f18b --- /dev/null +++ b/Sources/SwiftUICharts/Base/Extensions/ChartBase+Extension.swift @@ -0,0 +1,10 @@ +import SwiftUI + +extension View where Self: ChartBase { + public func data(_ data: [Double]) -> some View { + chartData.data = data + return self + .environmentObject(chartData) + .environmentObject(ChartValue()) + } +} diff --git a/Sources/SwiftUICharts/Base/Extensions/View+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/View+Extension.swift index 06e57edc..cea35f8f 100644 --- a/Sources/SwiftUICharts/Base/Extensions/View+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/View+Extension.swift @@ -1,11 +1,7 @@ import SwiftUI extension View { - public func type(_ type: S) -> some View where S: ChartType { - self.environment(\.chartType, AnyChartType(type)) - } - - public func style(_ style: ChartStyle) -> some View { - self.environment(\.chartStyle, style) + public func chartStyle(_ style: ChartStyle) -> some View { + self.environmentObject(style) } } diff --git a/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift b/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift index 6c5e273f..254c874a 100644 --- a/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift +++ b/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift @@ -1,8 +1,12 @@ import SwiftUI -public struct ChartGrid: View { +public struct ChartGrid: View, ChartBase { + public var chartData = ChartData() let content: () -> Content + @EnvironmentObject var data: ChartData + @EnvironmentObject var style: ChartStyle + public init(@ViewBuilder content: @escaping () -> Content) { self.content = content } diff --git a/Sources/SwiftUICharts/Base/Label/ChartLabel.swift b/Sources/SwiftUICharts/Base/Label/ChartLabel.swift index 03eba4d3..2a8fb07a 100644 --- a/Sources/SwiftUICharts/Base/Label/ChartLabel.swift +++ b/Sources/SwiftUICharts/Base/Label/ChartLabel.swift @@ -4,15 +4,13 @@ public enum ChartLabelType { case title case subTitle case largeTitle - case custom(size: CGFloat) + case custom(size: CGFloat, padding: EdgeInsets, color: Color) case legend } public struct ChartLabel: View { - @Environment(\.chartValue) private var chartValue: ChartValue - - @State var textToDisplay = "" - @State var isInteractionInProgress: Bool = false + @EnvironmentObject var chartValue: ChartValue + @State var textToDisplay:String = "" private var title: String @@ -26,11 +24,26 @@ public struct ChartLabel: View { return 24.0 case .largeTitle: return 38.0 - case .custom(let size): + case .custom(let size, _, _): return size } } + private var labelPadding: EdgeInsets { + switch labelType { + case .title: + return EdgeInsets(top: 16.0, leading: 8.0, bottom: 0.0, trailing: 8.0) + case .legend: + return EdgeInsets(top: 4.0, leading: 8.0, bottom: 0.0, trailing: 8.0) + case .subTitle: + return EdgeInsets(top: 8.0, leading: 8.0, bottom: 0.0, trailing: 8.0) + case .largeTitle: + return EdgeInsets(top: 24.0, leading: 8.0, bottom: 0.0, trailing: 8.0) + case .custom(_, let padding, _): + return padding + } + } + private let labelType: ChartLabelType private var labelColor: Color { @@ -43,8 +56,8 @@ public struct ChartLabel: View { return .black case .largeTitle: return .black - case .custom(_): - return .black + case .custom(_, _, let color): + return color } } @@ -55,18 +68,21 @@ public struct ChartLabel: View { } public var body: some View { - VStack (alignment: self.isInteractionInProgress ? .center : .leading) { + HStack { Text(textToDisplay) .font(.system(size: labelSize)) .bold() .foregroundColor(self.labelColor) + .padding(self.labelPadding) .onAppear { - self.textToDisplay = title + self.textToDisplay = self.title } .onReceive(self.chartValue.objectWillChange) { _ in self.textToDisplay = self.chartValue.interactionInProgress ? String(format: "%.01f", self.chartValue.currentValue) : self.title - self.isInteractionInProgress = self.chartValue.interactionInProgress } + if !self.chartValue.interactionInProgress { + Spacer() + } } } } diff --git a/Sources/SwiftUICharts/Base/Style/ChartStyle.swift b/Sources/SwiftUICharts/Base/Style/ChartStyle.swift index e23ede3a..c6e91f88 100644 --- a/Sources/SwiftUICharts/Base/Style/ChartStyle.swift +++ b/Sources/SwiftUICharts/Base/Style/ChartStyle.swift @@ -1,6 +1,6 @@ import SwiftUI -public struct ChartStyle { +public class ChartStyle: ObservableObject { public let backgroundColor: ColorGradient public let foregroundColor: [ColorGradient] diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift index 8edd0248..3b23be9b 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift @@ -1,31 +1,14 @@ import SwiftUI -public struct BarChart: ChartType { - public func makeChart(data: Self.Data, style: Self.Style) -> some View { +public struct BarChart: View, ChartBase { + public var chartData = ChartData() + + @EnvironmentObject var data: ChartData + @EnvironmentObject var style: ChartStyle + + public var body: some View { BarChartRow(chartData: data, style: style) } public init() {} } - -struct BarChart_Previews: PreviewProvider { - static var previews: some View { - Group { - BarChart().makeChart( - data: .init([0]), - style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack)) - Group { - BarChart().makeChart( - data: .init([1, 2, 3, 5, 1]), - style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack)) - }.environment(\.colorScheme, .light) - - Group { - BarChart().makeChart( - data: .init([1, 2, 3]), - style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack)) - }.environment(\.colorScheme, .dark) - - } - } -} diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift index f4f42865..feeae767 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift @@ -1,7 +1,7 @@ import SwiftUI public struct BarChartRow: View { - @Environment(\.chartValue) private var chartValue: ChartValue + @EnvironmentObject var chartValue: ChartValue @ObservedObject var chartData: ChartData @State var touchLocation: CGFloat = -1.0 @@ -39,7 +39,7 @@ public struct BarChartRow: View { .onChanged({ value in let width = geometry.frame(in: .local).width self.touchLocation = value.location.x/width - if let currentValue = getCurrentValue(width: width) { + if let currentValue = self.getCurrentValue(width: width) { self.chartValue.currentValue = currentValue self.chartValue.interactionInProgress = true } @@ -70,25 +70,3 @@ public struct BarChartRow: View { return self.chartData.data[index] } } - -//struct BarChartRow_Previews: PreviewProvider { -// static var previews: some View { -// Group { -// BarChartRow(data: [0], style: styleGreenRed) -// Group { -// BarChartRow(data: [1, 2, 3], style: styleGreenRed) -// BarChartRow(data: [1, 2, 3], style: styleGreenRedWhiteBlack) -// } -// Group { -// BarChartRow(data: [1, 2, 3], style: styleGreenRed) -// BarChartRow(data: [1, 2, 3], style: styleGreenRedWhiteBlack) -// }.environment(\.colorScheme, .dark) -// } -// } -//} -// -//private let styleGreenRed = ChartStyle(backgroundColor: .white, foregroundColor: .greenRed) -// -//private let styleGreenRedWhiteBlack = ChartStyle( -// backgroundColor: ColorGradient.init(.white), -// foregroundColor: [ColorGradient.redBlack, ColorGradient.whiteBlack]) diff --git a/Sources/SwiftUICharts/Charts/LineChart/Line.swift b/Sources/SwiftUICharts/Charts/LineChart/Line.swift index c152255d..6b5a9c19 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/Line.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/Line.swift @@ -1,7 +1,7 @@ import SwiftUI public struct Line: View { - @Environment(\.chartValue) private var chartValue: ChartValue + @EnvironmentObject var chartValue: ChartValue @State var frame: CGRect = .zero @ObservedObject var chartData: ChartData @@ -91,9 +91,15 @@ extension Line { private func getBackgroundPathView() -> some View { self.closedPath - .fill(style.backgroundColor.linearGradient(from: .bottom, to: .top)) + .fill(LinearGradient(gradient: Gradient(colors: [ + style.foregroundColor.first?.startColor ?? .white, + style.foregroundColor.first?.endColor ?? .white, + .white]), + startPoint: .bottom, + endPoint: .top)) .rotationEffect(.degrees(180), anchor: .center) .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + .opacity(0.2) .transition(.opacity) .animation(.easeIn(duration: 1.6)) } diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift b/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift index e86579a2..0d9b3040 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift @@ -1,32 +1,14 @@ import SwiftUI -public struct LineChart: ChartType { - public func makeChart(data: Self.Data, style: Self.Style) -> some View { - Line(chartData: data, style: style) - } +public struct LineChart: View, ChartBase { + public var chartData = ChartData() - public init() {} -} + @EnvironmentObject var data: ChartData + @EnvironmentObject var style: ChartStyle -struct LineChart_Previews: PreviewProvider { - static var previews: some View { - Group { - LineChart().makeChart( - data: .init([0]), - style: .init(backgroundColor: .white, foregroundColor: ColorGradient(.black))) - Group { - LineChart().makeChart( - data: .init([1, 2, 3, 5, 1]), - style: .init(backgroundColor: .white, foregroundColor: ColorGradient(.black))) - }.environment(\.colorScheme, .light) - - Group { - LineChart().makeChart( - data: .init([1, 2, 3]), - style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack)) - }.environment(\.colorScheme, .dark) - - } - + public var body: some View { + Line(chartData: data, style: style) } + + public init() {} } diff --git a/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift b/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift index 2907a191..7acfa27c 100644 --- a/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift +++ b/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift @@ -1,63 +1,14 @@ -// -// PieChart.swift -// SwiftUICharts -// -// Created by Nicolas Savoini on 2020-05-24. -// - import SwiftUI -public struct PieChart: ChartType { - public func makeChart(data: Self.Data, style: Self.Style) -> some View { +public struct PieChart: View, ChartBase { + public var chartData = ChartData() + + @EnvironmentObject var data: ChartData + @EnvironmentObject var style: ChartStyle + + public var body: some View { PieChartRow(chartData: data, style: style) } - public init() {} -} -struct PieChart_Previews: PreviewProvider { - static var previews: some View { - Group { - PieChart().makeChart( - data: .init([0]), - style: styleOneColor) - - Group { - PieChart().makeChart( - data: .init([56, 78, 53, 65, 54]), - style: styleOneColor) - PieChart().makeChart( - data: .init([56, 78, 53, 65, 54]), - style: styleTwoColor) - PieChart().makeChart( - data: .init([1, 1, 1, 1, 1, 1]), - style: trivialPursuit) - }.environment(\.colorScheme, .light) - - Group { - PieChart().makeChart( - data: .init([56, 78, 53, 65, 54]), - style: styleOneColor) - PieChart().makeChart( - data: .init([56, 78, 53, 65, 54]), - style: styleTwoColor) - PieChart().makeChart( - data: .init([1, 1, 1, 1, 1, 1]), - style: trivialPursuit) - }.environment(\.colorScheme, .dark) - - }.previewLayout(.fixed(width: 250, height: 400)) - } + public init() {} } - -private let styleOneColor = ChartStyle(backgroundColor: .white, foregroundColor: ColorGradient.init(.pink)) - -private let styleTwoColor = ChartStyle(backgroundColor: ColorGradient(.black), foregroundColor: [ColorGradient(.yellow), ColorGradient(.red)]) - -private let trivialPursuit = ChartStyle( - backgroundColor: .yellow, - foregroundColor: [ColorGradient(.yellow), - ColorGradient(.pink), - ColorGradient(.green), - ColorGradient(.primary), - ColorGradient(.blue), - ColorGradient(.orange)]) diff --git a/Sources/SwiftUICharts/Environment/Environment.swift b/Sources/SwiftUICharts/Environment/Environment.swift deleted file mode 100644 index b7e0130d..00000000 --- a/Sources/SwiftUICharts/Environment/Environment.swift +++ /dev/null @@ -1,30 +0,0 @@ -import SwiftUI - -extension EnvironmentValues { - var chartType: AnyChartType { - get { - return self[ChartTypeKey.self] - } - set { - self[ChartTypeKey.self] = newValue - } - } - - var chartStyle: ChartStyle { - get { - return self[ChartStyleKey.self] - } - set { - self[ChartStyleKey.self] = newValue - } - } - - var chartValue: ChartValue { - get { - return self[ChartValueKey.self] - } - set { - self[ChartValueKey.self] = newValue - } - } -} diff --git a/Sources/SwiftUICharts/Environment/EnvironmentKeys.swift b/Sources/SwiftUICharts/Environment/EnvironmentKeys.swift deleted file mode 100644 index 4cbcee81..00000000 --- a/Sources/SwiftUICharts/Environment/EnvironmentKeys.swift +++ /dev/null @@ -1,15 +0,0 @@ -import SwiftUI - -struct ChartTypeKey: EnvironmentKey { - static let defaultValue: AnyChartType = AnyChartType(BarChart()) -} - -struct ChartStyleKey: EnvironmentKey { - static let defaultValue: ChartStyle = ChartStyle(backgroundColor: .white, - foregroundColor: ColorGradient(ChartColors.orangeDark, - ChartColors.orangeBright)) -} - -struct ChartValueKey: EnvironmentKey { - static let defaultValue: ChartValue = ChartValue() -} From 3265d3e16b6480158b748095a227ea04c48f80ad Mon Sep 17 00:00:00 2001 From: Andras Samu Date: Sat, 25 Jul 2020 19:32:05 +0200 Subject: [PATCH 15/29] Add public modifier to ChartColors and add showShadow property --- Sources/SwiftUICharts/Base/CardView/CardView.swift | 13 +++++++++---- Sources/SwiftUICharts/Base/Style/Colors.swift | 8 ++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Sources/SwiftUICharts/Base/CardView/CardView.swift b/Sources/SwiftUICharts/Base/CardView/CardView.swift index 08b65c86..eae7856c 100644 --- a/Sources/SwiftUICharts/Base/CardView/CardView.swift +++ b/Sources/SwiftUICharts/Base/CardView/CardView.swift @@ -4,17 +4,22 @@ public struct CardView: View, ChartBase { public var chartData = ChartData() let content: () -> Content + private var showShadow: Bool + @EnvironmentObject var style: ChartStyle - public init(@ViewBuilder content: @escaping () -> Content) { + public init(showShadow: Bool = true, @ViewBuilder content: @escaping () -> Content) { + self.showShadow = showShadow self.content = content } public var body: some View { ZStack{ - RoundedRectangle(cornerRadius: 20) - .fill(Color.white) - .shadow(color: Color.gray, radius: 8) + if showShadow { + RoundedRectangle(cornerRadius: 20) + .fill(Color.white) + .shadow(color: Color.gray, radius: 8) + } VStack { self.content() } diff --git a/Sources/SwiftUICharts/Base/Style/Colors.swift b/Sources/SwiftUICharts/Base/Style/Colors.swift index 0d166936..860f27d4 100644 --- a/Sources/SwiftUICharts/Base/Style/Colors.swift +++ b/Sources/SwiftUICharts/Base/Style/Colors.swift @@ -2,9 +2,9 @@ import SwiftUI public enum ChartColors { // Orange - static let orangeBright = Color(hexString: "#FF782C") - static let orangeDark = Color(hexString: "#EC2301") + public static let orangeBright = Color(hexString: "#FF782C") + public static let orangeDark = Color(hexString: "#EC2301") - static let legendColor: Color = Color(hexString: "#E8E7EA") - static let indicatorKnob: Color = Color(hexString: "#FF57A6") + public static let legendColor: Color = Color(hexString: "#E8E7EA") + public static let indicatorKnob: Color = Color(hexString: "#FF57A6") } From 7fb2a0013c23e464eeeee9ffebb4535c719f8347 Mon Sep 17 00:00:00 2001 From: Andras Samu Date: Wed, 29 Jul 2020 18:49:57 +0200 Subject: [PATCH 16/29] Fix cornerMasking on card view when no shadow is set --- Sources/SwiftUICharts/Base/CardView/CardView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftUICharts/Base/CardView/CardView.swift b/Sources/SwiftUICharts/Base/CardView/CardView.swift index eae7856c..affb5977 100644 --- a/Sources/SwiftUICharts/Base/CardView/CardView.swift +++ b/Sources/SwiftUICharts/Base/CardView/CardView.swift @@ -23,7 +23,7 @@ public struct CardView: View, ChartBase { VStack { self.content() } - .clipShape(RoundedRectangle(cornerRadius: 20)) + .clipShape(RoundedRectangle(cornerRadius: showShadow ? 20 : 0)) } } } From 2ef73c84e2033341741c53580e57cd8602e27bb2 Mon Sep 17 00:00:00 2001 From: Sagar Patel Date: Fri, 31 Jul 2020 07:13:56 -0400 Subject: [PATCH 17/29] Dark/Light mode fixes (#148) Fix for making text work with both Dark/Light mode. Also solves line chart background to appear white in dark mode --- Sources/SwiftUICharts/Base/Label/ChartLabel.swift | 8 ++++---- Sources/SwiftUICharts/Charts/LineChart/Line.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftUICharts/Base/Label/ChartLabel.swift b/Sources/SwiftUICharts/Base/Label/ChartLabel.swift index 2a8fb07a..877bc2a1 100644 --- a/Sources/SwiftUICharts/Base/Label/ChartLabel.swift +++ b/Sources/SwiftUICharts/Base/Label/ChartLabel.swift @@ -49,13 +49,13 @@ public struct ChartLabel: View { private var labelColor: Color { switch labelType { case .title: - return .black + return Color(UIColor.label) case .legend: - return .gray + return Color(UIColor.secondaryLabel) case .subTitle: - return .black + return Color(UIColor.label) case .largeTitle: - return .black + return Color(UIColor.label) case .custom(_, _, let color): return color } diff --git a/Sources/SwiftUICharts/Charts/LineChart/Line.swift b/Sources/SwiftUICharts/Charts/LineChart/Line.swift index 6b5a9c19..bae3a53b 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/Line.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/Line.swift @@ -94,7 +94,7 @@ extension Line { .fill(LinearGradient(gradient: Gradient(colors: [ style.foregroundColor.first?.startColor ?? .white, style.foregroundColor.first?.endColor ?? .white, - .white]), + .clear]), startPoint: .bottom, endPoint: .top)) .rotationEffect(.degrees(180), anchor: .center) From ed01f5305d71f9b459290bfb824e769a83f8e8c2 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Mon, 24 Aug 2020 07:24:33 -0700 Subject: [PATCH 18/29] recalculate geometry if orientation has changed (#156) Co-authored-by: Dan Wood --- .../SwiftUICharts/Charts/LineChart/Line.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Sources/SwiftUICharts/Charts/LineChart/Line.swift b/Sources/SwiftUICharts/Charts/LineChart/Line.swift index bae3a53b..b2ff21e6 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/Line.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/Line.swift @@ -41,6 +41,13 @@ public struct Line: View { } public var body: some View { + + let orientationChanged = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification) + .makeConnectable() + .autoconnect() // see https://stackoverflow.com/a/62370919 + // This lets geometry be recalculated when device rotates. However it doesn't cover issue of app changing + // from full screen to split view. Not possible in SwiftUI? Feedback submitted to apple FB8451194. + GeometryReader { geometry in ZStack { if self.showFull && self.showBackground { @@ -56,7 +63,16 @@ public struct Line: View { } .onAppear { self.frame = geometry.frame(in: .local) + } + .onReceive(orientationChanged) { _ in + // When we receive notification here, the geometry is still the old value + // so delay evaluation to get the new frame! + DispatchQueue.main.async { + self.frame = geometry.frame(in: .local) // recalculate layout with new frame + } + } + .gesture(DragGesture() .onChanged({ value in self.touchLocation = value.location From 51db5a067a55c1cb3f968382cc974890f1364af2 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Mon, 24 Aug 2020 07:30:30 -0700 Subject: [PATCH 19/29] Issue 99 documentation (#159) * Starting on filling in documentation. * First pass on most/all files * more descriptions filled in * Some documentation but TBH the author would be better suited to explain how this works! * more basic stuff filled in * Add a description and bunch of discussion text for most of the view `body` declarations * more explanations Co-authored-by: Dan Wood --- .../Base/CardView/CardView.swift | 8 + .../SwiftUICharts/Base/Chart/ChartBase.swift | 1 + .../SwiftUICharts/Base/Chart/ChartData.swift | 3 + .../SwiftUICharts/Base/Chart/ChartValue.swift | 1 + .../Base/Extensions/Array+Extension.swift | 4 + .../Base/Extensions/CGPoint+Extension.swift | 6 + .../Base/Extensions/CGRect+Extension.swift | 4 +- .../Base/Extensions/ChartBase+Extension.swift | 4 + .../Base/Extensions/Color+Extension.swift | 2 + .../Base/Extensions/Path+QuadCurve.swift | 173 +++++++++++++++--- .../Base/Extensions/View+Extension.swift | 4 + .../SwiftUICharts/Base/Grid/ChartGrid.swift | 6 + .../SwiftUICharts/Base/Label/ChartLabel.swift | 20 +- .../SwiftUICharts/Base/Style/ChartStyle.swift | 27 ++- .../Base/Style/ColorGradient.swift | 17 +- Sources/SwiftUICharts/Base/Style/Colors.swift | 1 + .../Charts/BarChart/BarChart.swift | 4 + .../Charts/BarChart/BarChartCell.swift | 6 +- .../Charts/BarChart/BarChartRow.swift | 21 ++- .../Charts/LineChart/IndicatorPoint.swift | 6 +- .../SwiftUICharts/Charts/LineChart/Line.swift | 41 ++++- .../Charts/LineChart/LineChart.swift | 4 + .../Charts/PieChart/PieChart.swift | 4 + .../Charts/PieChart/PieChartCell.swift | 9 +- .../Charts/PieChart/PieChartRow.swift | 8 +- 25 files changed, 338 insertions(+), 46 deletions(-) diff --git a/Sources/SwiftUICharts/Base/CardView/CardView.swift b/Sources/SwiftUICharts/Base/CardView/CardView.swift index affb5977..9109a2b3 100644 --- a/Sources/SwiftUICharts/Base/CardView/CardView.swift +++ b/Sources/SwiftUICharts/Base/CardView/CardView.swift @@ -1,5 +1,6 @@ import SwiftUI +/// View containing data and some kind of chart content public struct CardView: View, ChartBase { public var chartData = ChartData() let content: () -> Content @@ -8,11 +9,18 @@ public struct CardView: View, ChartBase { @EnvironmentObject var style: ChartStyle + /// Initialize with view options and a nested `ViewBuilder` + /// - Parameters: + /// - showShadow: should card have a rounded-rectangle shadow around it + /// - content: <#content description#> public init(showShadow: Bool = true, @ViewBuilder content: @escaping () -> Content) { self.showShadow = showShadow self.content = content } + /// The content and behavior of the `CardView`. + /// + /// public var body: some View { ZStack{ if showShadow { diff --git a/Sources/SwiftUICharts/Base/Chart/ChartBase.swift b/Sources/SwiftUICharts/Base/Chart/ChartBase.swift index 3bc2f96a..9cf00102 100644 --- a/Sources/SwiftUICharts/Base/Chart/ChartBase.swift +++ b/Sources/SwiftUICharts/Base/Chart/ChartBase.swift @@ -1,5 +1,6 @@ import SwiftUI +/// Protocol for any type of chart, to get access to underlying data public protocol ChartBase { var chartData: ChartData { get } } diff --git a/Sources/SwiftUICharts/Base/Chart/ChartData.swift b/Sources/SwiftUICharts/Base/Chart/ChartData.swift index b3afb828..61beec79 100644 --- a/Sources/SwiftUICharts/Base/Chart/ChartData.swift +++ b/Sources/SwiftUICharts/Base/Chart/ChartData.swift @@ -1,8 +1,11 @@ import SwiftUI +/// An observable wrapper for an array of data for use in any chart public class ChartData: ObservableObject { @Published public var data: [Double] = [] + /// Initialize with data array + /// - Parameter data: Array of `Double` public init(_ data: [Double]) { self.data = data } diff --git a/Sources/SwiftUICharts/Base/Chart/ChartValue.swift b/Sources/SwiftUICharts/Base/Chart/ChartValue.swift index 368ae84e..48ff0c99 100644 --- a/Sources/SwiftUICharts/Base/Chart/ChartValue.swift +++ b/Sources/SwiftUICharts/Base/Chart/ChartValue.swift @@ -1,5 +1,6 @@ import SwiftUI +/// Representation of a single data point in a chart that is being observed public class ChartValue: ObservableObject { @Published var currentValue: Double = 0 @Published var interactionInProgress: Bool = false diff --git a/Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift index 1b2d837b..874430ca 100644 --- a/Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift @@ -1,6 +1,10 @@ import Foundation extension Array where Element == ColorGradient { + + /// <#Description#> + /// - Parameter index: offset in data table + /// - Returns: <#description#> func rotate(for index: Int) -> ColorGradient { if self.isEmpty { return ColorGradient.orangeBright diff --git a/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift index 07f3325c..a9a9b086 100644 --- a/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift @@ -1,6 +1,12 @@ import SwiftUI extension CGPoint { + + /// Calculate X and Y delta for each data point, based on data min/max and enclosing frame. + /// - Parameters: + /// - frame: Rectangle of enclosing frame + /// - data: array of `Double` + /// - Returns: X and Y delta as a `CGPoint` static func getStep(frame: CGRect, data: [Double]) -> CGPoint { let padding: CGFloat = 30.0 diff --git a/Sources/SwiftUICharts/Base/Extensions/CGRect+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/CGRect+Extension.swift index 020717cc..e66ee562 100644 --- a/Sources/SwiftUICharts/Base/Extensions/CGRect+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/CGRect+Extension.swift @@ -2,7 +2,9 @@ import Foundation import SwiftUI extension CGRect { - // Return the coordinate for a rectangle center + + /// Midpoint of rectangle + /// - Returns: the coordinate for a rectangle center public var mid: CGPoint { return CGPoint(x: self.midX, y: self.midY) } diff --git a/Sources/SwiftUICharts/Base/Extensions/ChartBase+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/ChartBase+Extension.swift index b1c8f18b..90060e3e 100644 --- a/Sources/SwiftUICharts/Base/Extensions/ChartBase+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/ChartBase+Extension.swift @@ -1,6 +1,10 @@ import SwiftUI extension View where Self: ChartBase { + + /// Set data for a chart + /// - Parameter data: array of `Double` + /// - Returns: modified `View` with data attached public func data(_ data: [Double]) -> some View { chartData.data = data return self diff --git a/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift index 8742decf..6f3bd22f 100644 --- a/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift @@ -1,6 +1,8 @@ import SwiftUI extension Color { + /// Create a `Color` from a hexadecimal representation + /// - Parameter hexString: 3, 6, or 8-character string, with optional (ignored) punctuation such as "#" init(hexString: String) { let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) var int = UInt64() diff --git a/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift b/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift index 9e9e3cdf..1da24afc 100644 --- a/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift +++ b/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift @@ -1,23 +1,37 @@ import SwiftUI extension Path { + + /// Returns a tiny segment of path based on percentage along the path + /// + /// TODO: Explain why more than 1 gets 0 and why less than 0 gets 1 + /// - Parameter percent: fraction along data set, between 0.0 and 1.0 (underflow and overflow are handled) + /// - Returns: tiny path right around the requested fraction func trimmedPath(for percent: CGFloat) -> Path { - // percent difference between points let boundsDistance: CGFloat = 0.001 let completion: CGFloat = 1 - boundsDistance let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent) - + + // Start/end points centered around given percentage, but capped if right at the very end let start = pct > completion ? completion : pct - boundsDistance let end = pct > completion ? 1 : pct + boundsDistance return trimmedPath(from: start, to: end) } - + + /// Find the `CGPoint` for the given fraction along the path. + /// + /// This works by requesting a very tiny trimmed section of the path, then getting the center of the bounds rectangle + /// - Parameter percent: fraction along data set, between 0.0 and 1.0 (underflow and overflow are handled) + /// - Returns: a `CGPoint` representing the location of that section of the path func point(for percent: CGFloat) -> CGPoint { let path = trimmedPath(for: percent) return CGPoint(x: path.boundingRect.midX, y: path.boundingRect.midY) } - + + /// <#Description#> + /// - Parameter maxX: <#maxX description#> + /// - Returns: <#description#> func point(to maxX: CGFloat) -> CGPoint { let total = length let sub = length(to: maxX) @@ -25,7 +39,9 @@ extension Path { return point(for: percent) } - var length: CGFloat { + /// <#Description#> + /// - Returns: <#description#> + var length: CGFloat { var ret: CGFloat = 0.0 var start: CGPoint? var point = CGPoint.zero @@ -56,7 +72,10 @@ extension Path { } return ret } - + + /// <#Description#> + /// - Parameter maxX: <#maxX description#> + /// - Returns: <#description#> func length(to maxX: CGFloat) -> CGFloat { var ret: CGFloat = 0.0 var start: CGPoint? @@ -107,7 +126,13 @@ extension Path { } return ret } - + + /// <#Description#> + /// - Parameters: + /// - points: <#points description#> + /// - step: <#step description#> + /// - globalOffset: <#globalOffset description#> + /// - Returns: <#description#> static func quadCurvedPathWithPoints(points: [Double], step: CGPoint, globalOffset: Double? = nil) -> Path { var path = Path() if points.count < 2 { @@ -126,7 +151,13 @@ extension Path { } return path } - + + /// <#Description#> + /// - Parameters: + /// - points: <#points description#> + /// - step: <#step description#> + /// - globalOffset: <#globalOffset description#> + /// - Returns: <#description#> static func quadClosedCurvedPathWithPoints(points: [Double], step: CGPoint, globalOffset: Double? = nil) -> Path { var path = Path() if points.count < 2 { @@ -149,7 +180,12 @@ extension Path { path.closeSubpath() return path } - + + /// <#Description#> + /// - Parameters: + /// - points: <#points description#> + /// - step: <#step description#> + /// - Returns: <#description#> static func linePathWithPoints(points: [Double], step: CGPoint) -> Path { var path = Path() if points.count < 2 { @@ -166,7 +202,12 @@ extension Path { } return path } - + + /// <#Description#> + /// - Parameters: + /// - points: <#points description#> + /// - step: <#step description#> + /// - Returns: <#description#> static func closedLinePathWithPoints(points: [Double], step: CGPoint) -> Path { var path = Path() if points.count < 2 { @@ -189,20 +230,39 @@ extension Path { } extension CGPoint { + + /// <#Description#> + /// - Parameters: + /// - to: <#to description#> + /// - x: <#x description#> + /// - Returns: <#description#> func point(to: CGPoint, x: CGFloat) -> CGPoint { let a = (to.y - self.y) / (to.x - self.x) let y = self.y + (x - self.x) * a return CGPoint(x: x, y: y) } - + + /// <#Description#> + /// - Parameter to: <#to description#> + /// - Returns: <#description#> func line(to: CGPoint) -> CGFloat { dist(to: to) } - + + /// <#Description#> + /// - Parameters: + /// - to: <#to description#> + /// - x: <#x description#> + /// - Returns: <#description#> func line(to: CGPoint, x: CGFloat) -> CGFloat { dist(to: point(to: to, x: x)) } - + + /// <#Description#> + /// - Parameters: + /// - to: <#to description#> + /// - control: <#control description#> + /// - Returns: <#description#> func quadCurve(to: CGPoint, control: CGPoint) -> CGFloat { var dist: CGFloat = 0 let steps: CGFloat = 100 @@ -217,7 +277,13 @@ extension CGPoint { } return dist } - + + /// <#Description#> + /// - Parameters: + /// - to: <#to description#> + /// - control: <#control description#> + /// - x: <#x description#> + /// - Returns: <#description#> func quadCurve(to: CGPoint, control: CGPoint, x: CGFloat) -> CGFloat { var dist: CGFloat = 0 let steps: CGFloat = 100 @@ -242,14 +308,26 @@ extension CGPoint { } return dist } - + + /// <#Description#> + /// - Parameters: + /// - to: <#to description#> + /// - t: <#t description#> + /// - control: <#control description#> + /// - Returns: <#description#> func point(to: CGPoint, t: CGFloat, control: CGPoint) -> CGPoint { let x = CGPoint.value(x: self.x, y: to.x, t: t, c: control.x) let y = CGPoint.value(x: self.y, y: to.y, t: t, c: control.y) return CGPoint(x: x, y: y) } - + + /// <#Description#> + /// - Parameters: + /// - to: <#to description#> + /// - control1: <#control1 description#> + /// - control2: <#control2 description#> + /// - Returns: <#description#> func curve(to: CGPoint, control1: CGPoint, control2: CGPoint) -> CGFloat { var dist: CGFloat = 0 let steps: CGFloat = 100 @@ -266,7 +344,14 @@ extension CGPoint { return dist } - + + /// <#Description#> + /// - Parameters: + /// - to: <#to description#> + /// - control1: <#control1 description#> + /// - control2: <#control2 description#> + /// - x: <#x description#> + /// - Returns: <#description#> func curve(to: CGPoint, control1: CGPoint, control2: CGPoint, x: CGFloat) -> CGFloat { var dist: CGFloat = 0 let steps: CGFloat = 100 @@ -293,14 +378,28 @@ extension CGPoint { return dist } - + + /// <#Description#> + /// - Parameters: + /// - to: <#to description#> + /// - t: <#t description#> + /// - control1: <#control1 description#> + /// - control2: <#control2 description#> + /// - Returns: <#description#> func point(to: CGPoint, t: CGFloat, control1: CGPoint, control2: CGPoint) -> CGPoint { let x = CGPoint.value(x: self.x, y: to.x, t: t, control1: control1.x, control2: control2.x) let y = CGPoint.value(x: self.y, y: to.y, t: t, control1: control1.y, control2: control2.x) return CGPoint(x: x, y: y) } - + + /// <#Description#> + /// - Parameters: + /// - x: <#x description#> + /// - y: <#y description#> + /// - t: <#t description#> + /// - c: <#c description#> + /// - Returns: <#description#> static func value(x: CGFloat, y: CGFloat, t: CGFloat, c: CGFloat) -> CGFloat { var value: CGFloat = 0.0 // (1-t)^2 * p0 + 2 * (1-t) * t * c1 + t^2 * p1 @@ -309,7 +408,15 @@ extension CGPoint { value += pow(t, 2) * y return value } - + + /// <#Description#> + /// - Parameters: + /// - x: <#x description#> + /// - y: <#y description#> + /// - t: <#t description#> + /// - control1: <#control1 description#> + /// - control2: <#control2 description#> + /// - Returns: <#description#> static func value(x: CGFloat, y: CGFloat, t: CGFloat, control1: CGFloat, control2: CGFloat) -> CGFloat { var value: CGFloat = 0.0 // (1-t)^3 * p0 + 3 * (1-t)^2 * t * c1 + 3 * (1-t) * t^2 * c2 + t^3 * p1 @@ -319,24 +426,42 @@ extension CGPoint { value += pow(t, 3) * y return value } - + + /// <#Description#> + /// - Parameters: + /// - point1: <#point1 description#> + /// - point2: <#point2 description#> + /// - Returns: <#description#> static func getMidPoint(point1: CGPoint, point2: CGPoint) -> CGPoint { return CGPoint( x: point1.x + (point2.x - point1.x) / 2, y: point1.y + (point2.y - point1.y) / 2 ) } - + + /// <#Description#> + /// - Parameter to: <#to description#> + /// - Returns: <#description#> func dist(to: CGPoint) -> CGFloat { return sqrt((pow(self.x - to.x, 2) + pow(self.y - to.y, 2))) } - + + /// <#Description#> + /// - Parameters: + /// - firstPoint: <#firstPoint description#> + /// - secondPoint: <#secondPoint description#> + /// - Returns: <#description#> static func midPointForPoints(firstPoint: CGPoint, secondPoint: CGPoint) -> CGPoint { return CGPoint( x: (firstPoint.x + secondPoint.x) / 2, y: (firstPoint.y + secondPoint.y) / 2) } - + + /// <#Description#> + /// - Parameters: + /// - firstPoint: <#firstPoint description#> + /// - secondPoint: <#secondPoint description#> + /// - Returns: <#description#> static func controlPointForPoints(firstPoint: CGPoint, secondPoint: CGPoint) -> CGPoint { var controlPoint = CGPoint.midPointForPoints(firstPoint: firstPoint, secondPoint: secondPoint) let diffY = abs(secondPoint.y - controlPoint.y) diff --git a/Sources/SwiftUICharts/Base/Extensions/View+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/View+Extension.swift index cea35f8f..2ddcea31 100644 --- a/Sources/SwiftUICharts/Base/Extensions/View+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/View+Extension.swift @@ -1,6 +1,10 @@ import SwiftUI extension View { + + /// Attach chart style to a View + /// - Parameter style: chart style + /// - Returns: `View` with chart style attached public func chartStyle(_ style: ChartStyle) -> some View { self.environmentObject(style) } diff --git a/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift b/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift index 254c874a..4562b37f 100644 --- a/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift +++ b/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift @@ -1,5 +1,6 @@ import SwiftUI +/// <#Description#> public struct ChartGrid: View, ChartBase { public var chartData = ChartData() let content: () -> Content @@ -7,10 +8,15 @@ public struct ChartGrid: View, ChartBase { @EnvironmentObject var data: ChartData @EnvironmentObject var style: ChartStyle + /// <#Description#> + /// - Parameter content: <#content description#> public init(@ViewBuilder content: @escaping () -> Content) { self.content = content } + /// The content and behavior of the `ChartGrid`. + /// + /// TODO: Explain why this is in a `ZStack` public var body: some View { ZStack{ self.content() diff --git a/Sources/SwiftUICharts/Base/Label/ChartLabel.swift b/Sources/SwiftUICharts/Base/Label/ChartLabel.swift index 877bc2a1..14a89fc8 100644 --- a/Sources/SwiftUICharts/Base/Label/ChartLabel.swift +++ b/Sources/SwiftUICharts/Base/Label/ChartLabel.swift @@ -1,5 +1,6 @@ import SwiftUI +/// What kind of label - this affects color, size, position of the label public enum ChartLabelType { case title case subTitle @@ -8,12 +9,15 @@ public enum ChartLabelType { case legend } +/// A chart may contain any number of labels in pre-set positions based on their `ChartLabelType` public struct ChartLabel: View { @EnvironmentObject var chartValue: ChartValue - @State var textToDisplay:String = "" + @State private var textToDisplay:String = "" private var title: String + /// Label font size + /// - Returns: the font size of the label private var labelSize: CGFloat { switch labelType { case .title: @@ -29,6 +33,8 @@ public struct ChartLabel: View { } } + /// Padding around label + /// - Returns: the edge padding to use based on position of the label private var labelPadding: EdgeInsets { switch labelType { case .title: @@ -44,8 +50,11 @@ public struct ChartLabel: View { } } + /// Which type (color, size, position) for label private let labelType: ChartLabelType + /// Foreground color for this label + /// - Returns: Color of label based on its `ChartLabelType` private var labelColor: Color { switch labelType { case .title: @@ -61,12 +70,19 @@ public struct ChartLabel: View { } } - public init (_ title: String, + /// Initialize + /// - Parameters: + /// - title: Any `String` + /// - type: Which `ChartLabelType` to use + public init(_ title: String, type: ChartLabelType = .title) { self.title = title labelType = type } + /// The content and behavior of the `ChartLabel`. + /// + /// Displays current value if chart is currently being touched along a data point, otherwise the specified text. public var body: some View { HStack { Text(textToDisplay) diff --git a/Sources/SwiftUICharts/Base/Style/ChartStyle.swift b/Sources/SwiftUICharts/Base/Style/ChartStyle.swift index c6e91f88..dafdaece 100644 --- a/Sources/SwiftUICharts/Base/Style/ChartStyle.swift +++ b/Sources/SwiftUICharts/Base/Style/ChartStyle.swift @@ -1,25 +1,44 @@ import SwiftUI +/// Descripton of colors/styles for any kind of chart public class ChartStyle: ObservableObject { - public let backgroundColor: ColorGradient + /// colors for background are of chart + public let backgroundColor: ColorGradient + /// colors for foreground fill of chart public let foregroundColor: [ColorGradient] + /// Initialize with a single background color and an array of `ColorGradient` for the foreground + /// - Parameters: + /// - backgroundColor: a `Color` + /// - foregroundColor: array of `ColorGradient` public init(backgroundColor: Color, foregroundColor: [ColorGradient]) { self.backgroundColor = ColorGradient.init(backgroundColor) self.foregroundColor = foregroundColor } - + + /// Initialize with a single background color and a single `ColorGradient` for the foreground + /// - Parameters: + /// - backgroundColor: a `Color` + /// - foregroundColor: a `ColorGradient` public init(backgroundColor: Color, foregroundColor: ColorGradient) { self.backgroundColor = ColorGradient.init(backgroundColor) self.foregroundColor = [foregroundColor] } - + + /// Initialize with a single background `ColorGradient` and a single `ColorGradient` for the foreground + /// - Parameters: + /// - backgroundColor: a `ColorGradient` + /// - foregroundColor: a `ColorGradient` public init(backgroundColor: ColorGradient, foregroundColor: ColorGradient) { self.backgroundColor = backgroundColor self.foregroundColor = [foregroundColor] } - + + /// Initialize with a single background `ColorGradient` and an array of `ColorGradient` for the foreground + /// - Parameters: + /// - backgroundColor: a `ColorGradient` + /// - foregroundColor: array of `ColorGradient` public init(backgroundColor: ColorGradient, foregroundColor: [ColorGradient]) { self.backgroundColor = backgroundColor self.foregroundColor = foregroundColor diff --git a/Sources/SwiftUICharts/Base/Style/ColorGradient.swift b/Sources/SwiftUICharts/Base/Style/ColorGradient.swift index 5446cd7d..0258d3c1 100644 --- a/Sources/SwiftUICharts/Base/Style/ColorGradient.swift +++ b/Sources/SwiftUICharts/Base/Style/ColorGradient.swift @@ -1,26 +1,35 @@ import SwiftUI +/// An encapsulation of a simple gradient between one color and another public struct ColorGradient: Equatable { - public let startColor: Color + public let startColor: Color public let endColor: Color + /// Initialize as a solid color + /// - Parameter color: a single `Color` (no gradient effect visible) public init(_ color: Color) { self.startColor = color self.endColor = color } - - public init (_ startColor: Color, _ endColor: Color) { + + /// Initialize a color gradient from two specified colors + /// - Parameters: + /// - startColor: starting color + /// - endColor: ending color + public init(_ startColor: Color, _ endColor: Color) { self.startColor = startColor self.endColor = endColor } + /// Convert to a `Gradient` object (more complicated than just two colors) + /// - Returns: a `Gradient` between the specified start and end colors public var gradient: Gradient { return Gradient(colors: [startColor, endColor]) } } extension ColorGradient { - /// Convenience method to return a LinearGradient from the ColorGradient + /// Convenience method to return a SwiftUI LinearGradient view from the ColorGradient /// - Parameters: /// - startPoint: starting point /// - endPoint: ending point diff --git a/Sources/SwiftUICharts/Base/Style/Colors.swift b/Sources/SwiftUICharts/Base/Style/Colors.swift index 860f27d4..1590fbcf 100644 --- a/Sources/SwiftUICharts/Base/Style/Colors.swift +++ b/Sources/SwiftUICharts/Base/Style/Colors.swift @@ -1,5 +1,6 @@ import SwiftUI +/// Some predefined colors, used for demos, defaults if color is missing, and data indicator point public enum ChartColors { // Orange public static let orangeBright = Color(hexString: "#FF782C") diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift index 3b23be9b..6ce279f7 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift @@ -1,11 +1,15 @@ import SwiftUI +/// A type of chart that displays vertical bars for each data point public struct BarChart: View, ChartBase { public var chartData = ChartData() @EnvironmentObject var data: ChartData @EnvironmentObject var style: ChartStyle + /// The content and behavior of the `BarChart`. + /// + /// public var body: some View { BarChartRow(chartData: data, style: style) } diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift index 16936138..761add15 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift @@ -1,5 +1,6 @@ import SwiftUI +/// A single vertical bar in a `BarChart` public struct BarChartCell: View { var value: Double var index: Int = 0 @@ -12,7 +13,7 @@ public struct BarChartCell: View { return Double(width)/(Double(numberOfDataPoints) * 1.5) } - @State var firstDisplay: Bool = true + @State private var firstDisplay: Bool = true public init( value: Double, index: Int = 0, @@ -28,6 +29,9 @@ public struct BarChartCell: View { self.touchLocation = touchLocation } + /// The content and behavior of the `BarChartCell`. + /// + /// Animated when first displayed, using the `firstDisplay` variable, with an increasing delay through the data set. public var body: some View { ZStack { RoundedRectangle(cornerRadius: 4) diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift index feeae767..4ab28f44 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift @@ -1,9 +1,10 @@ import SwiftUI +/// A single row of data, a view in a `BarChart` public struct BarChartRow: View { @EnvironmentObject var chartValue: ChartValue @ObservedObject var chartData: ChartData - @State var touchLocation: CGFloat = -1.0 + @State private var touchLocation: CGFloat = -1.0 enum Constant { static let spacing: CGFloat = 16.0 @@ -18,6 +19,11 @@ public struct BarChartRow: View { return max != 0 ? max : 1 } + /// The content and behavior of the `BarChartRow`. + /// + /// Shows each `BarChartCell` in an `HStack`; may be scaled up if it's the one currently being touched. + /// Not using a drawing group for optimizing animation. + /// As touched (dragged) the `touchLocation` is updated and the current value is highlighted. public var body: some View { GeometryReader { geometry in HStack(alignment: .bottom, @@ -51,11 +57,19 @@ public struct BarChartRow: View { ) } } - + + /// Value relative to maximum value + /// - Parameter index: index into array of data + /// - Returns: data value at given index, divided by data maximum func normalizedValue(index: Int) -> Double { return Double(chartData.data[index])/Double(maxValue) } + /// Size to scale the touch indicator + /// - Parameters: + /// - touchLocation: fraction of width where touch is happening + /// - index: index into data array + /// - Returns: a scale larger than 1.0 if in bounds; 1.0 (unscaled) if not in bounds func getScaleSize(touchLocation: CGFloat, index: Int) -> CGSize { if touchLocation > CGFloat(index)/CGFloat(chartData.data.count) && touchLocation < CGFloat(index+1)/CGFloat(chartData.data.count) { @@ -64,6 +78,9 @@ public struct BarChartRow: View { return CGSize(width: 1, height: 1) } + /// Get data value where touch happened + /// - Parameter width: width of chart + /// - Returns: value as `Double` if chart has data func getCurrentValue(width: CGFloat) -> Double? { guard self.chartData.data.count > 0 else { return nil} let index = max(0,min(self.chartData.data.count-1,Int(floor((self.touchLocation*width)/(width/CGFloat(self.chartData.data.count)))))) diff --git a/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift b/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift index 925ecc7f..d144fcd8 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift @@ -8,8 +8,12 @@ import SwiftUI +/// A dot representing a single data point as user moves finger over line in `LineChart` struct IndicatorPoint: View { - var body: some View { + /// The content and behavior of the `IndicatorPoint`. + /// + /// A filled circle with a thick white outline and a shadow + public var body: some View { ZStack { Circle() .fill(ChartColors.indicatorKnob) diff --git a/Sources/SwiftUICharts/Charts/LineChart/Line.swift b/Sources/SwiftUICharts/Charts/LineChart/Line.swift index b2ff21e6..d94bf6a6 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/Line.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/Line.swift @@ -1,21 +1,27 @@ import SwiftUI +/// A single line of data, a view in a `LineChart` public struct Line: View { @EnvironmentObject var chartValue: ChartValue - @State var frame: CGRect = .zero + @State private var frame: CGRect = .zero @ObservedObject var chartData: ChartData var style: ChartStyle - @State var showIndicator: Bool = false - @State var touchLocation: CGPoint = .zero + @State private var showIndicator: Bool = false + @State private var touchLocation: CGPoint = .zero @State private var showFull: Bool = false - @State var showBackground: Bool = true + @State private var showBackground: Bool = true var curvedLines: Bool = true - var step: CGPoint { + + /// Step for plotting through data + /// - Returns: X and Y delta between each data point based on data and view's frame + var step: CGPoint { return CGPoint.getStep(frame: frame, data: chartData.data) } + /// Path of line graph + /// - Returns: A path for stroking representing the data, either curved or jagged. var path: Path { let points = chartData.data @@ -28,6 +34,8 @@ public struct Line: View { return Path.linePathWithPoints(points: points, step: step) } + /// Path of linegraph, but also closed at the bottom side + /// - Returns: A path for filling representing the data, either curved or jagged var closedPath: Path { let points = chartData.data @@ -40,6 +48,11 @@ public struct Line: View { return Path.closedLinePathWithPoints(points: points, step: step) } + /// The content and behavior of the `Line`. + /// + /// Draw the background if showing the full line (?) and the `showBackground` option is set. Above that draw the line, and then the data indicator if the graph is currently being touched. + /// On appear, set the frame so that the data graph metrics can be calculated. On a drag (touch) gesture, highlight the closest touched data point. + /// TODO: explain rotation public var body: some View { let orientationChanged = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification) @@ -93,11 +106,17 @@ public struct Line: View { // MARK: - Private functions extension Line { + + /// Calculate point closest to where the user touched + /// - Parameter touchLocation: location in view where touched + /// - Returns: `CGPoint` of data point on chart private func getClosestPointOnPath(touchLocation: CGPoint) -> CGPoint { let closest = self.path.point(to: touchLocation.x) return closest } + /// Figure out where closest touch point was + /// - Parameter point: location of data point on graph, near touch location private func getClosestDataPoint(point: CGPoint) { let index = Int(round((point.x)/step.x)) if (index >= 0 && index < self.chartData.data.count){ @@ -105,6 +124,10 @@ extension Line { } } + /// Get the view representing the filled in background below the chart, filled with the foreground color's gradient + /// + /// TODO: explain rotations + /// - Returns: SwiftUI `View` private func getBackgroundPathView() -> some View { self.closedPath .fill(LinearGradient(gradient: Gradient(colors: [ @@ -120,6 +143,11 @@ extension Line { .animation(.easeIn(duration: 1.6)) } + /// Get the view representing the line stroked in the `foregroundColor` + /// + /// TODO: Explain how `showFull` works + /// TODO: explain rotations + /// - Returns: SwiftUI `View` private func getLinePathView() -> some View { self.path .trim(from: 0, to: self.showFull ? 1:0) @@ -149,5 +177,8 @@ struct Line_Previews: PreviewProvider { } } +/// Predefined style, black over white, for preview private let blackLineStyle = ChartStyle(backgroundColor: ColorGradient(.white), foregroundColor: ColorGradient(.black)) + +/// Predefined stylem red over white, for preview private let redLineStyle = ChartStyle(backgroundColor: .whiteBlack, foregroundColor: ColorGradient(.red)) diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift b/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift index 0d9b3040..379fbd37 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift @@ -1,11 +1,15 @@ import SwiftUI +/// A type of chart that displays a line connecting the data points public struct LineChart: View, ChartBase { public var chartData = ChartData() @EnvironmentObject var data: ChartData @EnvironmentObject var style: ChartStyle + /// The content and behavior of the `LineChart`. + /// + /// public var body: some View { Line(chartData: data, style: style) } diff --git a/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift b/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift index 7acfa27c..f23d3c96 100644 --- a/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift +++ b/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift @@ -1,11 +1,15 @@ import SwiftUI +/// A type of chart that displays a slice of "pie" for each data point public struct PieChart: View, ChartBase { public var chartData = ChartData() @EnvironmentObject var data: ChartData @EnvironmentObject var style: ChartStyle + /// The content and behavior of the `PieChart`. + /// + /// public var body: some View { PieChartRow(chartData: data, style: style) } diff --git a/Sources/SwiftUICharts/Charts/PieChart/PieChartCell.swift b/Sources/SwiftUICharts/Charts/PieChart/PieChartCell.swift index 7dedd730..bacc4f28 100644 --- a/Sources/SwiftUICharts/Charts/PieChart/PieChartCell.swift +++ b/Sources/SwiftUICharts/Charts/PieChart/PieChartCell.swift @@ -7,6 +7,7 @@ import SwiftUI +/// One slice of a `PieChartRow` struct PieSlice: Identifiable { var id = UUID() var startDeg: Double @@ -14,6 +15,7 @@ struct PieSlice: Identifiable { var value: Double } +/// A single row of data, a view in a `PieChart` public struct PieChartCell: View { @State private var show: Bool = false var rect: CGRect @@ -21,7 +23,9 @@ public struct PieChartCell: View { return min(rect.width, rect.height)/2 } var startDeg: Double - var endDeg: Double + var endDeg: Double + + /// Path representing this slice var path: Path { var path = Path() path.addArc( @@ -42,6 +46,9 @@ public struct PieChartCell: View { // Section color var accentColor: ColorGradient + /// The content and behavior of the `PieChartCell`. + /// + /// Fills and strokes with 2-pixel line (unless start/end degrees not yet set). Animates by scaling up to 100% when first appears. public var body: some View { Group { path diff --git a/Sources/SwiftUICharts/Charts/PieChart/PieChartRow.swift b/Sources/SwiftUICharts/Charts/PieChart/PieChartRow.swift index aae06735..fccf259f 100644 --- a/Sources/SwiftUICharts/Charts/PieChart/PieChartRow.swift +++ b/Sources/SwiftUICharts/Charts/PieChart/PieChartRow.swift @@ -7,6 +7,7 @@ import SwiftUI +/// A single "row" (slice) of data, a view in a `PieChart` public struct PieChartRow: View { @ObservedObject var chartData: ChartData @@ -27,7 +28,10 @@ public struct PieChartRow: View { return tempSlices } - + + /// The content and behavior of the `PieChartRow`. + /// + /// public var body: some View { GeometryReader { geometry in ZStack { @@ -72,10 +76,12 @@ struct PieChartRow_Previews: PreviewProvider { } } +/// Predefined color style, for preview private let defaultMultiColorChartStyle = ChartStyle( backgroundColor: Color.white, foregroundColor: [ColorGradient]()) +/// Predefined color style, for preview private let multiColorChartStyle = ChartStyle( backgroundColor: Color.purple, foregroundColor: [ColorGradient.greenRed, ColorGradient.whiteBlack]) From 8ee353c93adb6f69962ab0bb919ca94762c6b390 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Mon, 24 Aug 2020 07:31:17 -0700 Subject: [PATCH 20/29] Activity-type Rings charts (#161) Co-authored-by: Dan Wood --- .../Charts/RingsChart/Ring.swift | 187 ++++++++++++++++++ .../Charts/RingsChart/RingsChart.swift | 22 +++ .../Charts/RingsChart/RingsChartRow.swift | 133 +++++++++++++ 3 files changed, 342 insertions(+) create mode 100644 Sources/SwiftUICharts/Charts/RingsChart/Ring.swift create mode 100644 Sources/SwiftUICharts/Charts/RingsChart/RingsChart.swift create mode 100644 Sources/SwiftUICharts/Charts/RingsChart/RingsChartRow.swift diff --git a/Sources/SwiftUICharts/Charts/RingsChart/Ring.swift b/Sources/SwiftUICharts/Charts/RingsChart/Ring.swift new file mode 100644 index 00000000..dee10540 --- /dev/null +++ b/Sources/SwiftUICharts/Charts/RingsChart/Ring.swift @@ -0,0 +1,187 @@ +// +// Ring.swift +// ChartViewV2Demo +// +// Created by Dan Wood on 8/20/20. +// Based on article and playground code by Frank Jia +// https://medium.com/@frankjia/creating-activity-rings-in-swiftui-11ef7d336676 + +import SwiftUI + + +extension Double { + func toRadians() -> Double { + return self * Double.pi / 180 + } + func toCGFloat() -> CGFloat { + return CGFloat(self) + } +} + +struct RingShape: Shape { + /// Helper function to convert percent values to angles in degrees + /// - Parameters: + /// - percent: percent, greater than 100 is OK + /// - startAngle: angle to add after converting + /// - Returns: angle in degrees + static func percentToAngle(percent: Double, startAngle: Double) -> Double { + (percent / 100 * 360) + startAngle + } + private var percent: Double + private var startAngle: Double + private let drawnClockwise: Bool + + // This allows animations to run smoothly for percent values + var animatableData: Double { + get { + return percent + } + set { + percent = newValue + } + } + + init(percent: Double = 100, startAngle: Double = -90, drawnClockwise: Bool = false) { + self.percent = percent + self.startAngle = startAngle + self.drawnClockwise = drawnClockwise + } + + /// This draws a simple arc from the start angle to the end angle + /// + /// - Parameter rect: The frame of reference for describing this shape. + /// - Returns: A path that describes this shape. + func path(in rect: CGRect) -> Path { + let width = rect.width + let height = rect.height + let radius = min(width, height) / 2 + let center = CGPoint(x: width / 2, y: height / 2) + let endAngle = Angle(degrees: RingShape.percentToAngle(percent: self.percent, startAngle: self.startAngle)) + return Path { path in + path.addArc(center: center, radius: radius, startAngle: Angle(degrees: startAngle), endAngle: endAngle, clockwise: drawnClockwise) + } + } +} + +struct Ring: View { + + private static let ShadowColor: Color = Color.black.opacity(0.2) + private static let ShadowRadius: CGFloat = 5 + private static let ShadowOffsetMultiplier: CGFloat = ShadowRadius + 2 + + private let ringWidth: CGFloat + private let percent: Double + private let foregroundColor: ColorGradient + private let startAngle: Double = -90 + + private let touchLocation: CGFloat + + + + private var gradientStartAngle: Double { + self.percent >= 100 ? relativePercentageAngle - 360 : startAngle + } + private var absolutePercentageAngle: Double { + RingShape.percentToAngle(percent: self.percent, startAngle: 0) + } + private var relativePercentageAngle: Double { + // Take into account the startAngle + absolutePercentageAngle + startAngle + } + private var lastGradientColor: Color { + self.foregroundColor.endColor + } + + private var ringGradient: AngularGradient { + AngularGradient( + gradient: self.foregroundColor.gradient, + center: .center, + startAngle: Angle(degrees: self.gradientStartAngle), + endAngle: Angle(degrees: relativePercentageAngle) + ) + } + + init(ringWidth: CGFloat, percent: Double, foregroundColor: ColorGradient, touchLocation:CGFloat) { + self.ringWidth = ringWidth + self.percent = percent + self.foregroundColor = foregroundColor + self.touchLocation = touchLocation + } + + var body: some View { + GeometryReader { geometry in + ZStack { + // Background for the ring. Use the final color with reduced opacity + RingShape() + .stroke(style: StrokeStyle(lineWidth: self.ringWidth)) + .fill(lastGradientColor.opacity(0.142857)) + // Foreground + RingShape(percent: self.percent, startAngle: self.startAngle) + .stroke(style: StrokeStyle(lineWidth: self.ringWidth, lineCap: .round)) + .fill(self.ringGradient) + // End of ring with drop shadow + if self.getShowShadow(frame: geometry.size) { + Circle() + .fill(self.lastGradientColor) + .frame(width: self.ringWidth, height: self.ringWidth, alignment: .center) + .offset(x: self.getEndCircleLocation(frame: geometry.size).0, + y: self.getEndCircleLocation(frame: geometry.size).1) + .shadow(color: Ring.ShadowColor, + radius: Ring.ShadowRadius, + x: self.getEndCircleShadowOffset().0, + y: self.getEndCircleShadowOffset().1) + } + } + } + // Padding to ensure that the entire ring fits within the view size allocated + .padding(self.ringWidth / 2) + } + + private func getEndCircleLocation(frame: CGSize) -> (CGFloat, CGFloat) { + // Get angle of the end circle with respect to the start angle + let angleOfEndInRadians: Double = relativePercentageAngle.toRadians() + let offsetRadius = min(frame.width, frame.height) / 2 + return (offsetRadius * cos(angleOfEndInRadians).toCGFloat(), offsetRadius * sin(angleOfEndInRadians).toCGFloat()) + } + + private func getEndCircleShadowOffset() -> (CGFloat, CGFloat) { + let angleForOffset = absolutePercentageAngle + (self.startAngle + 90) + let angleForOffsetInRadians = angleForOffset.toRadians() + let relativeXOffset = cos(angleForOffsetInRadians) + let relativeYOffset = sin(angleForOffsetInRadians) + let xOffset = relativeXOffset.toCGFloat() * Ring.ShadowOffsetMultiplier + let yOffset = relativeYOffset.toCGFloat() * Ring.ShadowOffsetMultiplier + return (xOffset, yOffset) + } + + private func getShowShadow(frame: CGSize) -> Bool { + if self.percent >= 100 { + return true + } + let circleRadius = min(frame.width, frame.height) / 2 + let remainingAngleInRadians = (360 - absolutePercentageAngle).toRadians().toCGFloat() + + return circleRadius * remainingAngleInRadians <= self.ringWidth + } +} + +struct Ring_Previews: PreviewProvider { + static var previews: some View { + VStack { + Ring( + ringWidth: 50, percent: 5 , + foregroundColor: ColorGradient(.green, .blue), touchLocation: -1.0 + ) + .frame(width: 200, height: 200) + + Ring( + ringWidth: 20, percent: 110 , + foregroundColor: ColorGradient(.red, .blue), touchLocation: -1.0 + ) + .frame(width: 200, height: 200) + + + + } + } +} diff --git a/Sources/SwiftUICharts/Charts/RingsChart/RingsChart.swift b/Sources/SwiftUICharts/Charts/RingsChart/RingsChart.swift new file mode 100644 index 00000000..f70aed0e --- /dev/null +++ b/Sources/SwiftUICharts/Charts/RingsChart/RingsChart.swift @@ -0,0 +1,22 @@ +// +// RingsChart.swift +// ChartViewV2Demo +// +// Created by Dan Wood on 8/20/20. +// + +import SwiftUI + +public struct RingsChart: View, ChartBase { + public var chartData = ChartData() + + @EnvironmentObject var data: ChartData + @EnvironmentObject var style: ChartStyle + + // TODO - should put background opacity, ring width & spacing as chart style values + + public var body: some View { + RingsChartRow(width:10.0, spacing:5.0, chartData: data, style: style) + } + +} diff --git a/Sources/SwiftUICharts/Charts/RingsChart/RingsChartRow.swift b/Sources/SwiftUICharts/Charts/RingsChart/RingsChartRow.swift new file mode 100644 index 00000000..cbdbfbde --- /dev/null +++ b/Sources/SwiftUICharts/Charts/RingsChart/RingsChartRow.swift @@ -0,0 +1,133 @@ +// +// RingsChartRow.swift +// ChartViewV2Demo +// +// Created by Dan Wood on 8/20/20. +// + +import SwiftUI + +public struct RingsChartRow: View { + + var width : CGFloat + var spacing : CGFloat + + @EnvironmentObject var chartValue: ChartValue + @ObservedObject var chartData: ChartData + @State var touchRadius: CGFloat = -1.0 + + var style: ChartStyle + + public var body: some View { + GeometryReader { geometry in + + ZStack { + + // FIXME: Why is background circle offset strangely when frame isn't specified? See Preview below. Related to the .animation somehow ???? + Circle() + .fill(RadialGradient(gradient: self.style.backgroundColor.gradient, center: .center, startRadius: min(geometry.size.width, geometry.size.height)/2.0, endRadius: 1.0)) + + ForEach(0.. Bool { + let radius = min(size.width, size.height) / 2.0 + return index == self.touchedCircleIndex(maxRadius: radius) + } + + /// Find which circle has been touched + /// - Parameter maxRadius: radius of overall view circle + /// - Returns: which circle index was touched, if found. 0 = outer, 1 = next one in, etc. + func touchedCircleIndex(maxRadius: CGFloat) -> Int? { + guard self.chartData.data.count > 0 else { return nil } // no data + + // Pretend actual circle goes ½ the inter-ring spacing out, so that a touch + // is registered on either side of each ring + let radialDistanceFromEdge = (maxRadius + spacing/2) - self.touchRadius; + guard radialDistanceFromEdge >= 0 else { return nil } // touched outside of ring + + let touchIndex = Int(floor(radialDistanceFromEdge / (width + spacing))) + + if touchIndex >= self.chartData.data.count { return nil } // too far from outside, no ring + + return touchIndex + } + + /// Description + /// - Parameter maxRadius: radius of overall view circle + /// - Returns: percentage value of the touched circle, based on `touchRadius` if found + func getCurrentValue(maxRadius: CGFloat) -> Double? { + + guard let index = self.touchedCircleIndex(maxRadius: maxRadius) else { return nil } + return self.chartData.data[index] + } +} + + +struct RingsChartRow_Previews: PreviewProvider { + static var previews: some View { + + let multiStyle = ChartStyle(backgroundColor: ColorGradient(Color.black.opacity(0.05), Color.white), + foregroundColor: + [ColorGradient(.purple, .blue), + ColorGradient(.orange, .red), + ColorGradient(.green, .yellow), + ]) + + return RingsChartRow(width:20.0, spacing:10.0, chartData: ChartData([25,50,75,100,125]), style: multiStyle) + + // and why does this not get centered when frame isn't specified? + .frame(width:300, height:400) + } +} + + From 9210d01137157bb2481da7a34ee65890fc15fd63 Mon Sep 17 00:00:00 2001 From: Andras Samu Date: Mon, 24 Aug 2020 16:51:22 +0200 Subject: [PATCH 21/29] V2 beta 2 changes (#150) * Add PieChart interaction PR changes to v2 * Add custom string format for ChartLabel when interactionInProgress = true (#151) * Dark/Light mode fixes (#148) Fix for making text work with both Dark/Light mode. Also solves line chart background to appear white in dark mode * Add custom string format for ChartLabel when interactionInProgress = true Co-authored-by: Sagar Patel * Prepare charts to display x and y values souch as a value for a given point Co-authored-by: Roddy Munro Co-authored-by: Sagar Patel --- .../SwiftUICharts/Base/Chart/ChartData.swift | 14 ++- .../Base/Extensions/ChartBase+Extension.swift | 7 ++ .../SwiftUICharts/Base/Label/ChartLabel.swift | 11 ++- .../Charts/BarChart/BarChartRow.swift | 6 +- .../Charts/LineChart/IndicatorPoint.swift | 8 -- .../SwiftUICharts/Charts/LineChart/Line.swift | 25 +++-- .../Charts/PieChart/PieChartCell.swift | 7 -- .../Charts/PieChart/PieChartHelpers.swift | 34 +++++++ .../Charts/PieChart/PieChartRow.swift | 92 ++++++++----------- .../Charts/RingsChart/RingsChartRow.swift | 4 +- 10 files changed, 115 insertions(+), 93 deletions(-) create mode 100644 Sources/SwiftUICharts/Charts/PieChart/PieChartHelpers.swift diff --git a/Sources/SwiftUICharts/Base/Chart/ChartData.swift b/Sources/SwiftUICharts/Base/Chart/ChartData.swift index 61beec79..28c2065d 100644 --- a/Sources/SwiftUICharts/Base/Chart/ChartData.swift +++ b/Sources/SwiftUICharts/Base/Chart/ChartData.swift @@ -2,11 +2,23 @@ import SwiftUI /// An observable wrapper for an array of data for use in any chart public class ChartData: ObservableObject { - @Published public var data: [Double] = [] + @Published public var data: [(String, Double)] = [] + + var points: [Double] { + data.map { $0.1 } + } + + var values: [String] { + data.map { $0.0 } + } /// Initialize with data array /// - Parameter data: Array of `Double` public init(_ data: [Double]) { + self.data = data.map { ("", $0) } + } + + public init(_ data: [(String, Double)]) { self.data = data } diff --git a/Sources/SwiftUICharts/Base/Extensions/ChartBase+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/ChartBase+Extension.swift index 90060e3e..73873d78 100644 --- a/Sources/SwiftUICharts/Base/Extensions/ChartBase+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/ChartBase+Extension.swift @@ -6,6 +6,13 @@ extension View where Self: ChartBase { /// - Parameter data: array of `Double` /// - Returns: modified `View` with data attached public func data(_ data: [Double]) -> some View { + chartData.data = data.map { ("", $0) } + return self + .environmentObject(chartData) + .environmentObject(ChartValue()) + } + + public func data(_ data: [(String, Double)]) -> some View { chartData.data = data return self .environmentObject(chartData) diff --git a/Sources/SwiftUICharts/Base/Label/ChartLabel.swift b/Sources/SwiftUICharts/Base/Label/ChartLabel.swift index 14a89fc8..96a86303 100644 --- a/Sources/SwiftUICharts/Base/Label/ChartLabel.swift +++ b/Sources/SwiftUICharts/Base/Label/ChartLabel.swift @@ -12,7 +12,8 @@ public enum ChartLabelType { /// A chart may contain any number of labels in pre-set positions based on their `ChartLabelType` public struct ChartLabel: View { @EnvironmentObject var chartValue: ChartValue - @State private var textToDisplay:String = "" + @State var textToDisplay:String = "" + var format: String = "%.01f" private var title: String @@ -74,10 +75,12 @@ public struct ChartLabel: View { /// - Parameters: /// - title: Any `String` /// - type: Which `ChartLabelType` to use - public init(_ title: String, - type: ChartLabelType = .title) { + public init (_ title: String, + type: ChartLabelType = .title, + format: String = "%.01f") { self.title = title labelType = type + self.format = format } /// The content and behavior of the `ChartLabel`. @@ -94,7 +97,7 @@ public struct ChartLabel: View { self.textToDisplay = self.title } .onReceive(self.chartValue.objectWillChange) { _ in - self.textToDisplay = self.chartValue.interactionInProgress ? String(format: "%.01f", self.chartValue.currentValue) : self.title + self.textToDisplay = self.chartValue.interactionInProgress ? String(format: format, self.chartValue.currentValue) : self.title } if !self.chartValue.interactionInProgress { Spacer() diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift index 4ab28f44..366a3319 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift @@ -13,7 +13,7 @@ public struct BarChartRow: View { var style: ChartStyle var maxValue: Double { - guard let max = chartData.data.max() else { + guard let max = chartData.points.max() else { return 1 } return max != 0 ? max : 1 @@ -62,7 +62,7 @@ public struct BarChartRow: View { /// - Parameter index: index into array of data /// - Returns: data value at given index, divided by data maximum func normalizedValue(index: Int) -> Double { - return Double(chartData.data[index])/Double(maxValue) + return Double(chartData.points[index])/Double(maxValue) } /// Size to scale the touch indicator @@ -84,6 +84,6 @@ public struct BarChartRow: View { func getCurrentValue(width: CGFloat) -> Double? { guard self.chartData.data.count > 0 else { return nil} let index = max(0,min(self.chartData.data.count-1,Int(floor((self.touchLocation*width)/(width/CGFloat(self.chartData.data.count)))))) - return self.chartData.data[index] + return self.chartData.points[index] } } diff --git a/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift b/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift index d144fcd8..c0249477 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift @@ -1,11 +1,3 @@ -// -// IndicatorPoint.swift -// LineChart -// -// Created by András Samu on 2019. 09. 03.. -// Copyright © 2019. András Samu. All rights reserved. -// - import SwiftUI /// A dot representing a single data point as user moves finger over line in `LineChart` diff --git a/Sources/SwiftUICharts/Charts/LineChart/Line.swift b/Sources/SwiftUICharts/Charts/LineChart/Line.swift index d94bf6a6..942342de 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/Line.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/Line.swift @@ -16,14 +16,14 @@ public struct Line: View { /// Step for plotting through data /// - Returns: X and Y delta between each data point based on data and view's frame - var step: CGPoint { - return CGPoint.getStep(frame: frame, data: chartData.data) + var step: CGPoint { + return CGPoint.getStep(frame: frame, data: chartData.points) } /// Path of line graph /// - Returns: A path for stroking representing the data, either curved or jagged. var path: Path { - let points = chartData.data + let points = chartData.points if curvedLines { return Path.quadCurvedPathWithPoints(points: points, @@ -37,7 +37,7 @@ public struct Line: View { /// Path of linegraph, but also closed at the bottom side /// - Returns: A path for filling representing the data, either curved or jagged var closedPath: Path { - let points = chartData.data + let points = chartData.points if curvedLines { return Path.quadClosedCurvedPathWithPoints(points: points, @@ -47,20 +47,19 @@ public struct Line: View { return Path.closedLinePathWithPoints(points: points, step: step) } + + // see https://stackoverflow.com/a/62370919 + // This lets geometry be recalculated when device rotates. However it doesn't cover issue of app changing + // from full screen to split view. Not possible in SwiftUI? Feedback submitted to apple FB8451194. + let orientationChanged = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification) + .makeConnectable() + .autoconnect() /// The content and behavior of the `Line`. - /// /// Draw the background if showing the full line (?) and the `showBackground` option is set. Above that draw the line, and then the data indicator if the graph is currently being touched. /// On appear, set the frame so that the data graph metrics can be calculated. On a drag (touch) gesture, highlight the closest touched data point. /// TODO: explain rotation public var body: some View { - - let orientationChanged = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification) - .makeConnectable() - .autoconnect() // see https://stackoverflow.com/a/62370919 - // This lets geometry be recalculated when device rotates. However it doesn't cover issue of app changing - // from full screen to split view. Not possible in SwiftUI? Feedback submitted to apple FB8451194. - GeometryReader { geometry in ZStack { if self.showFull && self.showBackground { @@ -120,7 +119,7 @@ extension Line { private func getClosestDataPoint(point: CGPoint) { let index = Int(round((point.x)/step.x)) if (index >= 0 && index < self.chartData.data.count){ - self.chartValue.currentValue = self.chartData.data[index] + self.chartValue.currentValue = self.chartData.points[index] } } diff --git a/Sources/SwiftUICharts/Charts/PieChart/PieChartCell.swift b/Sources/SwiftUICharts/Charts/PieChart/PieChartCell.swift index bacc4f28..f733bcd7 100644 --- a/Sources/SwiftUICharts/Charts/PieChart/PieChartCell.swift +++ b/Sources/SwiftUICharts/Charts/PieChart/PieChartCell.swift @@ -1,10 +1,3 @@ -// -// PieChartCell.swift -// SwiftUICharts -// -// Created by Nicolas Savoini on 2020-05-24. -// - import SwiftUI /// One slice of a `PieChartRow` diff --git a/Sources/SwiftUICharts/Charts/PieChart/PieChartHelpers.swift b/Sources/SwiftUICharts/Charts/PieChart/PieChartHelpers.swift new file mode 100644 index 00000000..d9c68d8b --- /dev/null +++ b/Sources/SwiftUICharts/Charts/PieChart/PieChartHelpers.swift @@ -0,0 +1,34 @@ +import SwiftUI + +func isPointInCircle(point: CGPoint, circleRect: CGRect) -> Bool { + let r = min(circleRect.width, circleRect.height) / 2 + let center = CGPoint(x: circleRect.midX, y: circleRect.midY) + let dx = point.x - center.x + let dy = point.y - center.y + let distance = sqrt(dx * dx + dy * dy) + return distance <= r +} + +func degree(for point: CGPoint, inCircleRect circleRect: CGRect) -> Double { + let center = CGPoint(x: circleRect.midX, y: circleRect.midY) + let dx = point.x - center.x + let dy = point.y - center.y + let acuteDegree = Double(atan(dy / dx)) * (180 / .pi) + + let isInBottomRight = dx >= 0 && dy >= 0 + let isInBottomLeft = dx <= 0 && dy >= 0 + let isInTopLeft = dx <= 0 && dy <= 0 + let isInTopRight = dx >= 0 && dy <= 0 + + if isInBottomRight { + return acuteDegree + } else if isInBottomLeft { + return 180 - abs(acuteDegree) + } else if isInTopLeft { + return 180 + abs(acuteDegree) + } else if isInTopRight { + return 360 - abs(acuteDegree) + } + + return 0 +} diff --git a/Sources/SwiftUICharts/Charts/PieChart/PieChartRow.swift b/Sources/SwiftUICharts/Charts/PieChart/PieChartRow.swift index fccf259f..f812a17e 100644 --- a/Sources/SwiftUICharts/Charts/PieChart/PieChartRow.swift +++ b/Sources/SwiftUICharts/Charts/PieChart/PieChartRow.swift @@ -1,24 +1,18 @@ -// -// PieChartRow.swift -// SwiftUICharts -// -// Created by Nicolas Savoini on 2020-05-24. -// - import SwiftUI /// A single "row" (slice) of data, a view in a `PieChart` public struct PieChartRow: View { @ObservedObject var chartData: ChartData + @EnvironmentObject var chartValue: ChartValue var style: ChartStyle var slices: [PieSlice] { var tempSlices: [PieSlice] = [] var lastEndDeg: Double = 0 - let maxValue: Double = chartData.data.reduce(0, +) + let maxValue: Double = chartData.points.reduce(0, +) - for slice in chartData.data { + for slice in chartData.points { let normalized: Double = Double(slice) / (maxValue == 0 ? 1 : maxValue) let startDeg = lastEndDeg let endDeg = lastEndDeg + (normalized * 360) @@ -29,59 +23,47 @@ public struct PieChartRow: View { return tempSlices } - /// The content and behavior of the `PieChartRow`. - /// - /// + @State private var currentTouchedIndex = -1 { + didSet { + if oldValue != currentTouchedIndex { + chartValue.interactionInProgress = currentTouchedIndex != -1 + guard currentTouchedIndex != -1 else { return } + chartValue.currentValue = slices[currentTouchedIndex].value + } + } + } + public var body: some View { GeometryReader { geometry in ZStack { ForEach(0.. touchDegree }) ?? -1 + } else { + currentTouchedIndex = -1 + } + }) + .onEnded({ value in + currentTouchedIndex = -1 + }) + ) } } - -} - -struct PieChartRow_Previews: PreviewProvider { - static var previews: some View { - Group { - //Empty Array - Default Colors.OrangeStart - PieChartRow( - chartData: ChartData([8, 23, 32, 7, 23, 43]), - style: defaultMultiColorChartStyle) - .frame(width: 100, height: 100) - - PieChartRow( - chartData: ChartData([8, 23, 32, 7, 23, 43]), - style: multiColorChartStyle) - .frame(width: 100, height: 100) - - PieChartRow( - chartData: ChartData([8, 23, 32, 7, 23, 43]), - style: multiColorChartStyle) - .frame(width: 100, height: 100) - - }.previewLayout(.fixed(width: 125, height: 125)) - - } } - -/// Predefined color style, for preview -private let defaultMultiColorChartStyle = ChartStyle( - backgroundColor: Color.white, - foregroundColor: [ColorGradient]()) - -/// Predefined color style, for preview -private let multiColorChartStyle = ChartStyle( -backgroundColor: Color.purple, -foregroundColor: [ColorGradient.greenRed, ColorGradient.whiteBlack]) diff --git a/Sources/SwiftUICharts/Charts/RingsChart/RingsChartRow.swift b/Sources/SwiftUICharts/Charts/RingsChart/RingsChartRow.swift index cbdbfbde..7e2dd82b 100644 --- a/Sources/SwiftUICharts/Charts/RingsChart/RingsChartRow.swift +++ b/Sources/SwiftUICharts/Charts/RingsChart/RingsChartRow.swift @@ -39,7 +39,7 @@ public struct RingsChartRow: View { // make sure it doesn't get to crazy value ) - Ring(ringWidth:scaledWidth, percent: self.chartData.data[index], foregroundColor:self.style.foregroundColor.rotate(for: index), + Ring(ringWidth:scaledWidth, percent: self.chartData.points[index], foregroundColor:self.style.foregroundColor.rotate(for: index), touchLocation: self.touchRadius) @@ -108,7 +108,7 @@ public struct RingsChartRow: View { func getCurrentValue(maxRadius: CGFloat) -> Double? { guard let index = self.touchedCircleIndex(maxRadius: maxRadius) else { return nil } - return self.chartData.data[index] + return self.chartData.points[index] } } From 84578d2f6fa5aecd230ed8998dba570037db55ce Mon Sep 17 00:00:00 2001 From: Andras Samu Date: Mon, 24 Aug 2020 20:26:37 +0200 Subject: [PATCH 22/29] Add a public init() to RingsChart --- Sources/SwiftUICharts/Charts/RingsChart/RingsChart.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/SwiftUICharts/Charts/RingsChart/RingsChart.swift b/Sources/SwiftUICharts/Charts/RingsChart/RingsChart.swift index f70aed0e..1f59a651 100644 --- a/Sources/SwiftUICharts/Charts/RingsChart/RingsChart.swift +++ b/Sources/SwiftUICharts/Charts/RingsChart/RingsChart.swift @@ -19,4 +19,5 @@ public struct RingsChart: View, ChartBase { RingsChartRow(width:10.0, spacing:5.0, chartData: data, style: style) } + public init() {} } From 7861bbcad138feb91162cd448f0a54caa5ca639e Mon Sep 17 00:00:00 2001 From: Andras Samu Date: Wed, 9 Jun 2021 08:54:04 +0200 Subject: [PATCH 23/29] feat(core): refactoring chart dispalying (#191) now it is possible to add background lines precisely as charts are displayed at correct size also rewrote basics to conform with Shapes and Animatable protocol --- .../UserInterfaceState.xcuserstate | Bin 0 -> 37983 bytes .../xcdebugger/Breakpoints_v2.xcbkptlist | 6 + .../xcschemes/xcschememanagement.plist | 15 +- .../SwiftUICharts/Base/Chart/ChartData.swift | 16 +- .../Base/Extensions/CGPoint+Extension.swift | 2 +- .../SwiftUICharts/Base/Grid/ChartGrid.swift | 43 ++++- .../Charts/BarChart/BarChartCell.swift | 41 ++-- .../Charts/BarChart/BarChartCellShape.swift | 44 +++++ .../Charts/BarChart/BarChartRow.swift | 31 ++-- .../SwiftUICharts/Charts/LineChart/Line.swift | 175 +++++------------- .../LineChart/LineBackgroundShape.swift | 31 ++++ .../LineChart/LineBackgroundShapeView.swift | 19 ++ .../Charts/LineChart/LineShape.swift | 30 +++ .../Charts/LineChart/LineShapeView.swift | 26 +++ .../ArrayExtensionTests.swift | 7 - 15 files changed, 292 insertions(+), 194 deletions(-) create mode 100644 .swiftpm/xcode/package.xcworkspace/xcuserdata/samuandris.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 .swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist create mode 100644 Sources/SwiftUICharts/Charts/BarChart/BarChartCellShape.swift create mode 100644 Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShape.swift create mode 100644 Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShapeView.swift create mode 100644 Sources/SwiftUICharts/Charts/LineChart/LineShape.swift create mode 100644 Sources/SwiftUICharts/Charts/LineChart/LineShapeView.swift diff --git a/.swiftpm/xcode/package.xcworkspace/xcuserdata/samuandris.xcuserdatad/UserInterfaceState.xcuserstate b/.swiftpm/xcode/package.xcworkspace/xcuserdata/samuandris.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..a5f64f0a3fb188a684c45c82893ce8c09350485d GIT binary patch literal 37983 zcmeFa2Urx>*FQdY`hvjHdy^taul7P!lwPD*aA6l%ScXR zi5iUsYc%#4OEggvqfw*(Gc&ss(Y)sSeBbx?JpU)~EX>aB_kPa3XYM`cGX+%|wXQli z_%wzv93wCiqc9p{FrysL!78mzU0IRqsn8adsNq|tXLV&&j%Q^-sj9GAXMv#!OY-Gr z$r-&>!&J&BeWa5Zi^;Mwsuk5L4y`xGZ(zn)E6f(N!|bsjEEo&HLa{I`9E-prv36K{ ztOFK>MPo5o0+xcMVtufFSbt1`m0^Rh8f-W=3gfZG*ivj6_9?as`wUx$eSvMnwqo0` zUD$4HAGRO+20Mry!H!}lu#?ys>@0QxyNF%Eu3~>+Pq3%hpV%|(Irakk3ww#ZLIg6x zN>D3gfh>_Dazf6?1-T;+h7N3Go#cT08d?~&R zUyiT9SK^=ItMJwMI(##}1>cJA#`oag;0N(T_zC^IWGLBz>_~PeqsRoZ7ulOkB>RxrWDc22=8*+tAvug3 zPL3c)lB3Acsqlc&hj2IUQthZtR1_6Q^`x?>Tq=(mK;=_~l#(i;)KocDL1`%+HG~>U zjig3VA5tGt)2SKMOllT2o0>z-rRGx`sL!b{sEyPnYBRNk+DdJswo_kI-%tmsL)2mF z40V<|N1dm>r!G;KsUNAE)E(+B^(*y&dP@CCJ)>=CTiTAcryXcV+KG0iU1(R@jdrJ7 z(;l=p?Tb~={&WBxMu*ew==OA1x*Oet?nx)mD!Pa+rb}owT}qeH8oHdWpeyMrdJsK? z9!ig*C(@JX$@CO@Dm{&!L(iq>(I34o$Xx|Uu;ucbHBJLz5YUitw2HGP==mOe%w zr%%zR>GSjt^i}#t`ZoPL{h0oPenLNGaE4$=hGI+^7siwEV}h7qCY*_2IxroXu1qu& z!{jn~OkbuS)1Mi@q1yjHjG3AVosb)qpqnP)Y3Cu)hDl?0j&n#tDGM_SQna`Nd znJ<_v%vNS6vy0iw9ALg?zF`hBCz!L$73K!>BXg6v!`x>cGLM))m}ksO<`s*vGS-Z> zW1U!M)}3w5db4d?8I!_6hr3M#_w2mNHwJoyMY^rRU?0wnCviY(F zvW2omvZb1YYZnmNAuKwiT}V{>4&kwek{vKlEN~U(h&f@-m<#5L zxnb^DYs`bAIGSTPmXmQtoH5smGg*asVcu99%m?$u+G2i~KNi55a%Malz#|2ZbUgZy zM<4NM29IWPE>OE@tx8c=RjID1)%rM@ zE~l_wfT`YCB9??D3#cl9PU#E(wJHsC;$Rhran_twzC3hjz)+!X0h-Fz46&xRgItSl4cO5wxoWOU6obtJUQI3`KcWzFe*vTBWE^X2Njl3bpF0>b^?7JV1qV zVboPaQx(-EoD+w!oE7JA3e#ZakZA>0i9vGn zVS%4vb=7GMjpMO;TNlPl#$bF;XW+z#$Icb)r#M^-!v=27qF z_^cDL=$ie(1m%S-=T z-P^61>yUTUrI&)pPu3R^+ zI~T=8b1|#1d)R&P_W|}0djwa%bFo|x@wX?J%EiM~nxI!WNc}J9NRqY{s1p@BQGIl3 z8CX&|BuQPNE?0mi;Qq#7A*+~5*s@U+gJSCX#yANTgVj1UEWDszw#-GN7*<)+IA=o~ zkCwRHPs5K z!xNjg){x2y*flGwy z3F;?@TGt60jmMV{F!9GwfEV%+3h+j4xO6U~4%s6+$Uccn$tr^8>Rv0xkSYju00^RB z6oNug7z#%bC=#_p?YS(j50}m5aJgI_*O%+Z^U4dpyXFC{?z1%R6Q_7wW~IEkAApl}MOaRz5`8E%9d-qZ!ZY+(%GP74FZa0~OB?lA z+V(G&{u3+}3s@@QzA#{^>~FE8#j6D@>9|d`cn!Dt9kDbFA1PsJ1Yl{40F2RqrLAwk zlG=ccarguYOYZ@ew!b}zA^tRcj)0~2@elA1@sIH7_zZj|J`11C?c{cGySY8wm)u@% zAGe?TY85_LkEI2AEG^~^=&^K2kEQSa#nOL*r8NSU)^cAPu(Teqgm1t<7e3rK+(Gf< zEH{-q8is8fzC*y%cJ6R3zLWd*9r5%fz8}DW?*$F&2xw6FSD-;11w^%&o5($ef)3+H zB|IGgJRKA8^c5sH+9HV|em(w!fTvUVY5WX+7C(od$G^uf;1{_Q+)1vUJH?&m&TwbB zbKLn=_$57_u1VTBev|uNkEe@zZT#xLc=}K9^iaUlBkqC$Pmlk0ZhC=(DpQO9#r;r= zzv3>v8J30S%eSRxp}(v>%1iTO^C4Z?(wi&!Ge087^ZO9TT6uDlIPgbm>a zSR!l*JHnoDARGxN!kKU(T)7+EkK9e}C+-$^o4do^<$hj8xJy_fyrj8_@a695vGl8c zZuL?NLhRNNEpDfcJ$jC;<#;Qr!Xa<6!VttN`~kSZfIL^-|zyH8Z{u#+e5 zit>o&kui^2!PMM?M;8Bo!Tq})tyZGg#C;(Y;+(|E+x5+Cr0eMdyj zAZ81QoQZ6BBoiPx2ifz;=uMbpuX%J2YqncTEFx;Lz@@||#A0Fzk4$)E$|JL-U}SQi zSjNM)E?~x7z>K44mZ0xrU0|xZq5=#rq%Mc;<>sLou$r3tSqgG8hf5Y(^7^W8=&{YjUO`Z{5L=0D#CBo_v6I+E>?ZaQ zU-HPB7y84NM|Ql>9S%Hl6+_QU3cU55ze^WX|)*%^)%t1(ErIxFmdd<-``eMNmi+(hdZLG$qYQIcZK>kd~ws zX-(RYARi$-3guB4kHUEr!J|kXwc}BH9(CYR$JM00Bq*da=|Z}aZrFVihN6=}P`dIc zhDWhH>M00H!oPy@zbhzY5C{qh_A{OJfamnq1`}3yei^&pFO_q{nq=qc#Q6i6$cm#=4cm&N!;}K+;!K2JoWTjp{bdokt z4&hOjK{~P}eV&KIsecco|M_VkIZi zx$lVd+2lL{DK_L>4hA|;K+1dpDScZY#T>0M6tGRBHiXxJGp~L z1w1O`g|$n?qaq#^^QeSJY95vHsBAU4i`)&?y7$SwpZgTf#lY2j8%ZnZ*Ntg6n4?G0wP(se9o)P<^4Fc>YW)RyO|b#WSXv0&Z+7K=?| zB~)5 zx@lp8g0B%K+7^|FsjMi}f+M=3NvRUdKOsmkKy98|rn*|A5)Zk_pU8X5$y?-Y z@(y{I{Fz5Xc{GMc<9IZYM^kz9zM<~FkdK7A|4Kd}AM$7zkB0MT#8UD%@^|tvk4Exn z6puy=b@y&jTC=LAYSoomFwM@F`~7{yXhmUJhN?=T6~-`M?)&$#r7}b_U61apv>+8? zZeBR%mMa<^36CU(w!Eb9H?)N!C{h>)jz_#W4j$d5e6Tb(z-DxfTBCFWS#UEv2WhbZ1DGMIKB=#PUCU7nu-Q`k#=^fqPDDRXV<@AP(h-H)ug?4eDxp{J#H5Vuy!--{)XTe#k1vE|sE4bd+@g4mqF5)`IjbZq@JcNN3e&7dcWt;w0uTlsTV1}w1yc~*kO`^!99K}kCnbzBwcI{r< z4SWo2gz7}cIoHL+a^{>xU3?GD5-i)T;c*=j8FCc-7%H_1N|+?nMQRlYl5In1=CCSN zV~8A+)Ah+IscCOA>~Cf|UYD8&ELHzqJ2=^f%lVjq;lh5#%sXp~ptu!1ObPN>!0*af!OL462Y_qOMj!-G$i` zrev5X#V=s!no$KVGa%IvR->z~EEh2)-i!{$VuIa5lA_EHga@uKQsa+e)7nZey-JR23?%y3nl$_!hYJP{3N_78)1| zb6%xj6Dmac@BWl)YJ_?VKd$g;r7O$m4gUk7{^OO3SmC-oTz^rhh|ADl-yN*(lML70 zF$`~ARUMrU*S+Anpr$M<3ap3c&*k1Tfovkj(6viwR!|2x4+DsuW)Q!$8QpYF2bv#@TEz$NA?n8U&JyjYJ zXV53~3vErh&~|7e6R)bs68ccU1*g!)^?+;WD`pqg2Pwd@rV?CiR9GSS#9YJNFde4G z28&n4;6GdrA2+ChKYV&XtP1#OA%+esgZssBeX}84_%5Un(o{i6B~}D6HIUA5*G+Wf zk!t4^r@Ssfp&i6P~%foHRpr0gaIocwp0tZvD$of;qeb2=-d6KQEORx zK(ld6X^IOGDasbOfbyw&>IC@2p2pnZ|Bv7)dmFAUQa`}&_YL()h58%tAoRV|2gT42 z(zixr6^eI+vCzP;FfJ|Y**IoWK0=%(IUqtTSHNtPn2Qiv4j*A;Rbrn?<5vUM)i5eW zV(I`uOk*uyM-=3CaO5sK%l*NK$8z32AghSupi54KZa*+nPq0Mwud0clZr8?G9HSrmamAO>H5x zr)eA0SX0M_{^%i&qfie?Le=7bL0%eXv0Ml>AVFwf3503I7z%xRo647fAvdgPD}@CC6^DKro zx+C3&j-%U%d5f*1W5Js}9&@8Zg!^<5-3zWe2(hYFLqYezu$aoKVOn)@Nwr%PXgMmk zgo?sIf49)!5Ln#=ok@s;VZWt`x)8ECT3DkUEZrl8e-vf}`ji!LU$|f%uv!N|tJ-0m zV9ksJ9UukE1TWJ8(2f!qNiFp3NQ?)s#;M@sI2)W37X$CbDv|SIEAUvR09Y=bBZL_6Z?FXWN>}8C{J^&T;LuU0sI1s==Jno`XqgWe#|h8Jun1xVN!qt zpc=gXKL$_#FPVDq<$uZ=v#x9?8_(vk73^4c4*1*eVe7%G{<%yp^OAK0KY68WnCwIF zeBU8EF1smvZe(uM#;A)?rctR8Z#2(nozd4uKN$UHY;4@xxPx(;vD%n3o@czl_>l26 z<3C$jv})Tbrd7XIHLX5qwX)UTR^PY!&BVmS+oZcmo=LUI2PUgbzB0LN^3>GQG|)7` zRB1ZKbb;v>(|Xfi%w%R>W>IDX%!ZrIG23W%-0YrQCij-d$QAO@@&)qk@^kXX=9cCm z<|*do=F`kqn;$m6ZNXT0Tf|u?Eyi0cwfM^7h9zO?VHsmtXgSVundR4(H?3%^HdYB% zrB+j|KC?Pz^}yP~I^4RC^-${t*1N5*+7LG0HVHNwn-6WiusLh<%+}d9%2s7N*>;_6 zz3me_N4xHJD!VCm>+R0iJ+pVSkFzhcpKia^{*nXf(AFW%VTi*g4qrRmbF^~ooV5l%~;jygSdc5&|ET;=?+^HZN0wrB@ZKy_8vtZvpn{D zJn(e&O!6G*xyJK?m&~iZSBckLuY+DsyxVwZdr$P<>V3P7U7Lh9!`rNB^Mj9xPdA@I zK6O5)eQDqJzGc3Pd{4B++eWl4X}h59aX;KI(ogNT$nT^-<=?@-!oSY{TtKUUsDQx% zYXYtX+5{#Bjt$%z_)CyyP+rjVpo2j#gCl}9u)19gu?XoEGB#vK$fMA8}Y!|7GTo-w#ooBm&?H0B>*WRjqYWr#J z4|SkAM0Xg|VONJgJ4SY_?)XK=UpobKD(kem)9ub}I;%P_>wKe2>n@5ewOy`ub?Z8? z>yoZlySaB$bgS!jy}L(uW%m``Z$vDq`SujT~hjLJEZ>yWF?-ImAX z^~qb2_qcCd-`Ra{_KWB@q2Jm5ZTb)Ge|Uhy0L_5i`DXcw{EY*tf!PCB4}76WQPe3O z7bFxcEO<~DQ#iNqXJvQgEae?l7u5{at)ebPGm35(cPXA(e5a&a$()jV>KOHW^~2Jh zrHf0Sl%_5+@c7}&N01{5N9-Tz zK9V1KZB*2#+RI)Noip{tw8ClMz8~`by!T&yQ1rpE z47C@8Rj#F&$ux&Y37z$ZnLJ$`h9l(?89@y=PaIUG`D8%)p?2Y zwtejR@kbxOm|rsg%!23z>lZpLoV@VKBITm`Pr84yZn5*?sf(X2Q7<`P+oN_%op;@w zrS#IlOMhC{XW7B!?U%1w;jm)rioaG?ti1YZ`lkn0wOh4nwbSYk*Whbv*4$a!Z|#ZC zVn5rqE?`~ldYkoAH((oTHr)Mu;OA$*=>5h1jqNwC+vK%r;bzOtQ?{Tj!?rxwTC(-( zw%lzexA)qeQ^6}oPz4!JN@4K--fB(g= za=to!Anm}huX}%e=$rU&zB(9n@XJG84(&SJ@$mL<+kd zUwD3z10{aSCHbW}mz^%xUGcrL{%Yja-PdBT9lf4${o;+n8$bW3`|-ui@jsdTH0PG< ztyQ-}Z|}Mjf9J&AzISi_T=nzwdlT-P-(UEP&o7&P?f&c02RRRJKGZ&Z^=R5}_P?$C zJ^c6mk5eCC{-gYl=TD|QwSW5QpY8uV_$>R`t>;5tFfZo)<@?u;m%U&9@T&aPE7ahs=MQ%xytPS%Zv4JO$BdHp`@*qCDiDT=SC zStJ12L>iU>Pr@UyalnZ%6I+7Sfflg>+W@RtUt-^4-(knGlh|os$~q5RS=Zn>?|0B4 zNMsJ)jqbpN<&At$Tkt3B1iV*ys2?f>{Xq@PSLJ9B(t#c^1f0TVfxpmq=qkDiELL~W z&!9a#Mo)mn>IHgQ}2rRsHog z)>hWO*038O{NPCXkB!m)*Z;GQ;?We*1Cwe)`Sfqt^^1kg=cRDV7t4a3IN_UNYut|t z1XdI{HIU8>M%0nNQE-M0#|6_kmp;LPVI9JN>jPXmscf%>70ucHM%jXbR(X;w*=BjUlOo^tAXv}eJYVkqLQf; zDwRs3(y0utH3w!8vv@RzNAq|zpGOOM^a+n3WgX|-ghGPK5&WciH2eRR??2UFs=(a; zD^KkPIvJ`!D*5C8d+&6rSSotK{{tQXN+XrM=>M45f*K^1yZFCDKp?V}h^JwHN8&*Z zmI|%?KV^BKMo6VD{oiJS5m^VQ(bNPia3wW{;wYXPOO2z(Q*cVRoJT8ow30`k@(2#= zSMzAiN@^l@pPCFYrV1zOJX$LppnoQYuM&8R1t%G$=(&fdY%$q|kxg%d$9Se2TL1*ihzSvN2|LB~n~ zQunH~K(i(cj~0doq9y2nbrV*vtb~MEINeInB`MTEHU=b6@oKHE8WO7m`0VP(3OeDUQ(G2W0nvr7288wC6)s-ojy#CTD zEZ9&5yTJh#423unaNYD16Vt9kWWNU4 zhzK49O-o9KF>jh~!D2`?a8?Mak^e?$dQ1(d0EK$5xzw$=9#M~GvRX4|0f4rs3M3Kqyy47%(B zYB|^lQVXd?)F;$pY6(?K)lo~SWjtEXqYXUzoJU{qXd{m{@n|!Tw(w{xP$p6R%ttVv}+l)m)b||r@rFR zZXO-x5t!GDE|+eiCxC$mv7u^#4U;1@&All@uTuG&#Q|qXeC}TmU-D=#kM=Et1FDn4aTSmD^9bsB04{|7w~AM5s#OvxW%MxN zlphuasFlDt*-#_lHi%JH1ssUKA+nv6hf0~1ATHkx`55YTfx3vk*HYJcpmzAXRP|sF^b>W9x(%i*FeM5aM5@3>4K$cql@81w zL~=zQ9pcf!6mz;~^Hq7|sByVqE>W)cn32OOR~=PY2!?$sCFmfk0XotAKebW^z^kn2 zFL5fVlnIbVw_ff-%dH$-+`asRLnGUF?iSOdcXE1WPQQVL#p?1wHACweR%Vu21sseL z8)Hvkuv;!J*n8b8~Cc^sNT`Z`Q3|fngB((n$(GQ<$yp1s|?#*k}{9C4=$7c#Fx16XC2n>4HB0e9p_eUrq#IMfHxIQdA;$73N{W?Du|#85!4u0oXgITQqJRr0Zh8%*eQ_N%J<;caMsG*L>Dzx0XXbv1Ut+;^MPJW;$4q zn_0|?H|u*Q^m^Ak*De}&9P&&|s&k8zll5Ts^v)^2?)N>U%t)#;imL~`>z&gr8g~`a z<@T*}%gatoh>1(ih?7(G{RhB~?YqGk>UqhE^xX>p?rVf9DpA$03iwX3N?b5Al7i3B!huc-OjY z_+ntODA}7zD^zu(xwdBH7J zVNs!R1ly4=K%%Pw=KR5+&P)J>W+Ap3`yAULo*kaXZh$??L+lBn;H=OY6dOO8Q$iIQiY5Y|{cN-V&Ih*wbNxQ_6*`12qPyrf;HZBNJA0PENAHIRi%LyzJP$9# zOF*$10cy=mI2&98p7opX9r$rjX>P&3-YbG67(xbWjXQA0cObeFQNR_SOOz0UfE|7q z!GoXSB;bKx4czbBz&_wxV1B<&JS3h0yE}r@LVMtI448 z=5@l-lTuSH(6|XYhx9H3;ii63TuR-iei6uT-!Xo^ydXcbx<^c(kVqiKZ7c%_l|v&N zzcwjSFmBYf(-)WDCnG;Bq%oejG-QC1RZ-md_>rG4{~t9J!@B&C`W^N)sYldrJUYUo zqjjM1fPMkW&MrZZ@Ora?A}TV0@JLakH+XOTck!BQGtX%j3tUURp#Gv>Qm<%?Ml?uy1W|dMjoRlm_A-16Y#A&saT1jPs(TJ-MKmsh`LQq3S%~J{66cQbG z%VbUvCMeK?!6&FZPzf07!ZPv6NGI&m2!XojVM5iU+Ujor$+*BKEj&=2v zg{E3HK468m3-x#F5F7@Z9s+MQ?b&3z8P3*ymhkA3C?j-R+7FCK!O*vkZVU8<-K)Bb zQly6!9Y_bmhBh6A{U~;bkLS(Qxho@VuN*M#1STShr4Wy|Fu@YN09U+z4 zF!>oW?Lc=DGVREto3)r1Y*|5`j$*S@Dm26N!zxIreq0Rcy3;X$aXN~Q=Fu%4-3GT{ z>^?vPcDw)p4H+Z?$-7D!t*I<5gI;mXNpA=d_5ncYjMr9{H>miA%zM$<0B5>4ok%Cq z$#e>xN~h83bOxPCXVHClbeBgz^XMLrV8`tj9{tLr2Rwp9ghxCAk^7xTkJr*U*dscR z?o0Qh`_lvHeE6oI3xqAYKe%`vhZlDcJWldB#p5)OGr~Q49zQPC-`w!PlK}?vBBeC2 zp+~F0ydSo1hN{cq`Ch;fc(LdzD@2W7przJ#mh?0vJqNhyzW}4X8thrb?7+uCaM6$+ zqVt-?HV94Q1C^Mffk+(YAU!{R;|@o2^&L9dO#jRvD!H)P1YDw3Qjvyi#rMm=j!09z ze}JK3f=mdGGS-IJDuq}H@it^{338&ZgxE{c5 zJ^f9^&4A)&@i+#>;YfG}GZXs=-WUK+(n9}ubnmxU%m`)>Y25P@RYldAmFa@5lN>1i z^nCNk2&>`@3^`zG^9dFR&w?&_DOIY9gbLUW)SaR0=%p}=ETfmxE9jNvjB?ntY`iUO=?bj&DMf~~jbS`)!{ag@x8re^i_ez_H7rFExL_I< zh9|aC$g=SfyC{+9goY!%7{-`hL9sth=mx^Ty-==IU!|hC1z`Qg4G9-kHz%h z93?dK6j&DvuQFIkU%(PM4<2aqFsY%J=*u8_09C_S8OFJez9O{mO+ix1YxH&LD52TN zHTGBYX`qh2A!H{UJ-kzVX`pYykt+QYk2^FG%{%lju)#v#rGKXH(f4`Wk;k2Q+?mH+ zmeIe`59o*J8jrj3_$nSh1}unz^h;7FY`ZI@T^4AUbgw7uQ^2w&F4wxg0`z*TE0nPP z(q#TL%q)M>e}SJA{fvH2zu<8<9(U*Q)=TM^z_Kxx$2~-f4MHf-LXf8@ojz}Ix&bjU z%)!FmyC5C_z1U&!9t(A$;f~m5h6Zqh0UXQ77$e4*X~pB7JRZyAeRy2O% zf;Jy6sGLrit{XRCVfC&M_XqVN`g(lxW}!kkP2Yt9sSWjMR3RGNgLpie$Afr0n8&?% zJVyA6mT=}xrX&zr(GCXE9=qgQSu{vgoFo(Y}tmTX}jj_cN znGaz`nogxKGkH7{WSqlzJWdqxEWy?V&uA*+vza*t8HYx|x+Q4|l8ggu@(N}lvxxbG zSlXBK4oZ#?wU;R8 z4I8}%*)j!?YZzL*hUkVH~hTDw#RXP2}x-d7wab3;`d-V-6m4ZYoL zpbR&gkB{N*FVeu|zw5y0)4fq;q6(w|`S?E^kd{qqF#s$Q27o1n0l?m8Sst&^4?vMP z0AL@6S2yW@)`&GW^uLbBi<|X7D~JAP%~=c9lC@&3Sx~o2c)XOyH9TIy<5fJaZRmg2 zUV=C9Hwf^~6AmHtZ^@9PgLRQ2lt~d9Pbu_0#(GHM#8z9NYpm&xJ z?6j!C!ionP>_2^TS(65~9E8r|Aar9K*eGngFbMDI2VsafT}|LxzcvbSY`kF<#`E~l z=A+PC7==VOiA`ox*i<%+$A|Iw2p%8B<70T7=kalkqmU`}({Qn$T0W2%`Z`zYtC1}p zBG>^^29m1Z^dW*RltQ?_8G;fi+p+IH1Wg|zT2?m)9{vZzW3R4d7Y|`<*|-#0f~Vc4Qw__pyrGjJXmayTo*;DcM0OH8nVrH;WvB5tJYT`z z4|x0|9-qPEGaEbkLkX!s<6yG* zem(mX8vZBy47Sn1eGY7*^e?V#qCxWb8Xo_y<;(z+FZ+^x1v5kE#>tn*K`~n^&J6Fp zKKsfj8Eu#tw)6OB%_at!F-#1yRx%Trsmu(#!eCEm9glC|@h^CM6OV7<@ofzggUm{j z*7bis`^xO4Y4YP!$>23Y!mlh=7Kh+k)|1B% z80Ld-c>G(g7*1mFBmX`p{s$9+EE6UKSr(5QXv<|e;&b9#h{R<1G6hTsGSH3>*2+Mq zKJ;!Af~-iU2Kkm117#k57%21L_}89Oy@4^W;ax%o$W+O6;>;k^3Nyn|m>H0e?oBTx z0zV0Gs2naGzqQ;ptCNAd#M=TvKQL^n^<6CHj&3q^Z57TihYf9$q{z8g$;G7tcLCUhQoq9X$M?5AjuW?zlF$#;y#eg z6pH&$_K|G5YzB{?;qkLPevZe_14AJ82-(9KRS)26PS&4PiA-X`>7tN{c(#Iqx*YLo1RI?w#a}+zr_)VY^Rj|sy=N~k?dGS_RdN6@9h4lEqX=_)lU z>F87#Aux8Z2kH`kECfhGx`8iFAFLlXfcAmI$WC+&oXusyAzMER1YQ7c4zKwxV~4UM*ioQoj%CNQ)7g*N|ZiM#>&iPF7RS*4|p-RkF2dM9$bvmWtp-*vNBnXY?5r23qb8s{bY38=&mtk%oxkSP1(fQ%-G!6(%9PA*4W{>ar8r^DYtM^-d*lK#KN3C9&AQQrbGEtihHW_L% z++?K5C6l`*_e_2&`U4f5UcFXj8>Uzz)wN17*?_cl)ghvqc%4D&4WZ1Y_6zUCF?Rpwgr zYV*P7L(PYqk2D``&Y6!jUu?d|{JI5c;bW0(p|zN9vB6@y#V(6I7JDrYThv>ewm55X z-r|DA4;Gg#u2|f&_|@W}#qSn>SUk0~wrp+L#?sf)&obCD)H2*M(lX96%`(HXpJji` ze9IEca?8QsEIr9`isdxR>6SAsXIsv-Tx7Y{a=YbG%VU-&EbA>#Tb{K%Z+XG;2g}Qr z4=sPQd~Er|@=wd>mVa5kvO-pb6=l`hD#}V_HPLE~)k&+z;E)z(-PyXEb(D3Cb)t2a zb+&b`bzkfL*7?>N>q_fE*45U7!2x@=b)EG}>s8iktT$MHVZF(Ei}lym$E=TApR_)0 zeaZTY^*!r{*1uUlwti;)!uq8RW@BdKWD{f)ViRT)Vbjj0gH0!!E;ik4qHHp4`q<>y z%ZF6jg*v_$CY+Gl$%yxzCdfRQbJ8XB^ z?y=o#yWjRWIC`J9J!kv9?M2(Cc9flwT`N0NJ4-ujJ8)01^Re@@3%851i?{1(*UK)& zF3m2(F3V17H^^?h-2}Ty;2J*7?gP7z>}J@_vYTVK%5II_XW%UUx!p#)&30Suw%hHr z+iiEj?oWGX`)>Ay_LJ>Dvp;J8v;FV(PwfA+e-2LMCJtr}<_?w))(*A~;0)p53~uEC z4nYo~4&e@w4jB#u9aIj*4r+%AhbjlHL$$*=hxZ(2ILvkU*kOUg5{Ej6WezJGHaqNh zIOA~6;d_UR4woFRI9zkM;c(O8mcuiL7Y;8SF-P2ybfg_wMl)-5;u_}K-nFA^XK<@ea?Nuscdc|CDQ{A)Nv)yyu`?{C9m%9&lALGuuk8_{oKE-{S z`v>le+*iBrcK_0SpZiztU%MZ4KkRapBorNKX1C>Dk`1qi1K&uAbdJqdoIIM|;+J9`L;FW$G2` zmF89DHOyzdb3UbnsO zdj0D4(Cas^$KIqj<89?_@9pUA?A_Yi)7#tI$Ge?(taq+=U+@0j`Q8feLT{CKvA5d0 z%)7#Soc9FpN#0Yur+I(qJ;Qsp_gwFfy%&0a;=R-Rg7?cdZf#=Ql(w1OW_z3SZJzjG zK7DPHwDs}#3H0gY)72-+C)THjPl8X8kIrYP&j_DUK4X2} z^O@)~*=L^50-t3*pZjd{+3B;#XP?g@pCdls`JC{n_j%xJ=IiO}!_XFSQzO#Ji`pySBg(be5eYg4U@ZIIR$9JFa0pEkZ z-})Z)J??wb_m{RNZA02-v>noRQQJdp@A(<~we<@F!i5fgo%~|`lKfKrGW`1Z<@)vY z>+hHEr}3-w)B4r;4fPxDH`i~O-zvYge(U@;`fc&s=C{M|u-{R?Gk(|o?)csF`_=D} z-=BUj{9gJaf5PA0zq@~yf40BUU+rH86b?H78vmjG!~G}w&+%X5zs~=2|4sf|{df59 z_TTIOmH#*Xhy5@6U-Q4=f7Ab#|6Tw4{tx_r^MCCB)c;w4Z9rr|UcktJB>~?AJPfoB z3=hl*>=&3HSP-ZTEDszUI5coX;HW?jC?UoNP7jdatU$|@(A({@(Jn^)Gugw(8!?4K_3Kt6f`qv zUeNrYML~;$)&=bhIu>*?=ycGzpbJ5lg02SL2>L1LPSA^>SHUQl2&RJBV58tx!KT6H z!Ir_n!70JI;Mu|3gRg`TA+1BAfQ%w3BsC;Gq;H5aq$s2$q%=emQV}vDWON80GCpKt z$mEa}Asa)s0b#}Nko_SCLJo!;4mlrkG30j0{g7Wn9)&y!`7`82$jeZZP=`?GP`6Nz zQ14Km(8$o((6rE^P<5y#v@%p1S`#`nbVTUrP(E~g=*OW8Ll=kEhAs_V5xOdLZRq;Y zFG4qkZVBBRdM@;O=<_hsFt4zVVJTt7VO-dpuusEwggp<(!)4)C;qKu+K#>s`9unR@ zyi<7B@Tl;Z@WJ8J!&irI4Br#JFZ@9G!SHXxzY9MRekuIN@LS<`!|#Xx9{wc!S@>V! zuOfUR`b3P4m>Mx7VqwIphz$`NBeq0rkJuaWRm3+Dha-+eoQ^mf@pHt}h*yz#Bo)a< znnhYfT1VPNIz)OzdPjywMn(3F>>ZgLnHHH7**9`Pq$08~vNTc`IU@0L; zCDBQiNp?w&NiInqN#04mNq$M8Nu7WYBP}T-sb5llk|If&G$^S$X>ii8q>)LZlO`rj zNt%}QVbZ*$kCPTAEl=8!v@2;((!QhvNe7d@P5LhBMAE6Gvq|TZZYJGIx|4J-IXAf| zxg@zXxjgw)^3~+)$v-CFN&za#l>RCCDFrFtrkqRpKILM{<<#g@urnGkE8{pbxiA=)-^3EZE@P#w4G^t()OhtNIRJJZQ6HfC(^E@T~GTl?N-{|wEJnl zraesiEgetyOYf6DG<|#ene+!4vJ8vti=VlN`64 z);V4|J~@6lfjJ>L;W?2x9df`KEk}`4lQSu2Va}?YFLUa1uIAj&`6K7goEJH-a`9Z_ zT+>|hT&rB$T>IS4xv9AYxnpxb%$=9JAor8p+T3NipXF}I-I%*2cYE&6-1^*~a$n^+ z0u+4|I~A7|KPw(6 zepCFRc&>O^fC`8Lzk<|){sn^zh82t~7*jB|;Jt!L1yc(?D41R_t6*-y{DO@ITMD)p z>?-)OU|+$3f^P~A6&xw}uHZz$se&^FKNh?wloz%wj4K>iII?g-;p)OYg{KN{6~0t5 zN>*vCv{G6t?Ue4yHcEeGgfdzgr|hZhtxQ&?Dyx;Fl%tira*A@Ua=vnra*47|xlFlV zd0cr~c}{sjc}00mc|&b!icc1wF1}cNx%g`FjpCcdPfD;7>k``%&l2yFu#)hSh>~_C(Iwd>r6t2krj>kJ zvZiER$>$}TO16~TE_qn;QjOH4no%36P1JI=h1yl!RvoMkQ%9)Vt9z+4)LQir^&It5 z^$PW;>NV;O>fP$S>iz1k)!(YmsV}I1P+w8sS3gt#rG8b4ml~DYmO7L=mAaPtmj;%G zl!lddE=@1ZE-fz|Tv}VYqV#0xh0;r?_-j%%1sYB>OS43?N3&0JKyy&@t>!z;3C$_Z8O>eIea!>SZ<;?ePyfG~?)<5% zDu4pGBH)&hX)21jLZB(iSZR!iisEgHteM4FY|clUeWz4yKQULv`qV+~EIp(v7D znwlmF*45lAIK0urtI-b<&&>&J+hZA2>Umde{mMgipYHI2$?;Lkc}u2&vVyE4Ysgx%fovk5k*~-%q>Ah&M@T(6 zP0o;Wq?z0#cW4xiq4(1JXm{F^Cew%LNct!pO()PvG@Cw4^JqSuNoUcI=%=)X9-s$l z9X(9zX#;JfP4p`Lo8F{%SQLw9omfxSoAqS_SQ1NN53->woju0JvP_oEa#&5yWvrO3WUJX0R>{6)Rctq_VZX2j*2tRJU+ee(d@bL=H}Ng}bH0OD@!fn6ujWVjAN(vo&oA&65hY?o zThT$pi3HI{^b-R`ns{7fh)gkFWQ$ypCteXV1Qt{X;fjT#NbC^h;!E+h*d@LfdqlOU z5l!NxI4#bI^P)Kd7RAVRvZK6Pc9vabcNs5}WQu%Hrpl}cWt1axWuAOfN~xtM136c| zFF%kU%4Kq$+#$PTdD)G1+q-wUaqhkDeQpo8mwUh4&mG}D z;*N6D-O=tCH^a?z$GKVVME7-fp8Ki0&%LO+t8_J8DK$qGs`+Z6DpJL2rCOy*Rhe3= zwyT}0LRG47Rh4Q~XVnFDNwui!>ZZD_Tj@BRp!?|oI!O=KLv*ShuE*%<8f&e69qIzT zNUzfy^=7?a*XlZbSl8=c^)Y>1pVHU$Ez`=hF|nqd>1^&d{mek~fEi?lm|-T(JZ>^f zrWtQ?%xmTiGut?Zn$Q%OLNnhiHp|TlQ({WZR`a#lWxg{%n0=<+{AzwP$4#@jXs(#6 zUK_8Q7w;u_eZ2l&qL=Iq_J(@Hy-aVsm*q|LCV9_z&v`F+)4b_kuBW`EUZvOQNBPPA z)BbFKpG=r8j>_RIXu{uY0mzuo`BFZXx*d;K5%eg04WLBH9*8NsBYZLIBRJJ~qf z+4i+5_FNju(V*@j$nEC@CQKLiJZL&1^YXwVoO3$6sWBD#4@ zL>KQE_J}Cw{lmmCIZO@H!V%#k;ixbpoEW|s`M1xHY`d?AFeD)hW#lBhAbc+@iu@}t Y3qKB5{5$+b$e;hZ{oeoj{|vYN15fB^BLDyZ literal 0 HcmV?d00001 diff --git a/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 00000000..867e4fe8 --- /dev/null +++ b/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcschemes/xcschememanagement.plist b/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcschemes/xcschememanagement.plist index a0f26bbb..1be8e537 100644 --- a/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,20 @@ SwiftUICharts.xcscheme_^#shared#^_ orderHint - 2 + 0 + + + SuppressBuildableAutocreation + + SwiftUICharts + + primary + + + SwiftUIChartsTests + + primary + diff --git a/Sources/SwiftUICharts/Base/Chart/ChartData.swift b/Sources/SwiftUICharts/Base/Chart/ChartData.swift index 28c2065d..b598975c 100644 --- a/Sources/SwiftUICharts/Base/Chart/ChartData.swift +++ b/Sources/SwiftUICharts/Base/Chart/ChartData.swift @@ -12,8 +12,20 @@ public class ChartData: ObservableObject { data.map { $0.0 } } - /// Initialize with data array - /// - Parameter data: Array of `Double` + var normalisedPoints: [Double] { + points.map { $0 / (points.max() ?? 1.0) } + } + + var normalisedRange: Double { + (normalisedPoints.max() ?? 0.0) - (normalisedPoints.min() ?? 0.0) + } + + var isInNegativeDomain: Bool { + (points.min() ?? 0.0) < 0 + } + + /// Initialize with data array + /// - Parameter data: Array of `Double` public init(_ data: [Double]) { self.data = data.map { ("", $0) } } diff --git a/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift index a9a9b086..363cd2c3 100644 --- a/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift @@ -8,7 +8,7 @@ extension CGPoint { /// - data: array of `Double` /// - Returns: X and Y delta as a `CGPoint` static func getStep(frame: CGRect, data: [Double]) -> CGPoint { - let padding: CGFloat = 30.0 + let padding: CGFloat = 0 // stepWidth var stepWidth: CGFloat = 0.0 diff --git a/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift b/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift index 4562b37f..ecaf270b 100644 --- a/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift +++ b/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift @@ -1,25 +1,52 @@ import SwiftUI -/// <#Description#> public struct ChartGrid: View, ChartBase { public var chartData = ChartData() let content: () -> Content + let numberOfHorizontalLines = 4 @EnvironmentObject var data: ChartData @EnvironmentObject var style: ChartStyle - /// <#Description#> - /// - Parameter content: <#content description#> public init(@ViewBuilder content: @escaping () -> Content) { self.content = content } - /// The content and behavior of the `ChartGrid`. - /// - /// TODO: Explain why this is in a `ZStack` public var body: some View { - ZStack{ - self.content() + HStack { + ZStack { + VStack { + ForEach(0.. Path { + let baseLine: CGFloat = CGFloat(frame.height / 2) + var hLine = Path() + hLine.move(to: CGPoint(x:0, y: baseLine)) + hLine.addLine(to: CGPoint(x: frame.width, y: baseLine)) + return hLine + } + + var body: some View { + GeometryReader { geometry in + line(frame: geometry.frame(in: .local)) + .stroke(Color(white: 0.3), style: StrokeStyle(lineWidth: 1, lineCap: .round, dash: [5, 10])) } } } diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift index 761add15..e457d3cd 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift @@ -4,27 +4,17 @@ import SwiftUI public struct BarChartCell: View { var value: Double var index: Int = 0 - var width: Float - var numberOfDataPoints: Int var gradientColor: ColorGradient var touchLocation: CGFloat - var cellWidth: Double { - return Double(width)/(Double(numberOfDataPoints) * 1.5) - } - - @State private var firstDisplay: Bool = true + @State private var didCellAppear: Bool = false public init( value: Double, index: Int = 0, - width: Float, - numberOfDataPoints: Int, gradientColor: ColorGradient, touchLocation: CGFloat) { self.value = value self.index = index - self.width = width - self.numberOfDataPoints = numberOfDataPoints self.gradientColor = gradientColor self.touchLocation = touchLocation } @@ -33,20 +23,15 @@ public struct BarChartCell: View { /// /// Animated when first displayed, using the `firstDisplay` variable, with an increasing delay through the data set. public var body: some View { - ZStack { - RoundedRectangle(cornerRadius: 4) - .fill(gradientColor.linearGradient(from: .bottom, to: .top)) - } - .frame(width: CGFloat(self.cellWidth)) - .scaleEffect(CGSize(width: 1, height: self.firstDisplay ? 0.0 : self.value), anchor: .bottom) - .onAppear { - self.firstDisplay = false + BarChartCellShape(value: didCellAppear ? value : 0.0) + .fill(gradientColor.linearGradient(from: .bottom, to: .top)) .onAppear { + self.didCellAppear = true } .onDisappear { - self.firstDisplay = true + self.didCellAppear = false } .transition(.slide) - .animation(Animation.spring().delay(self.touchLocation < 0 || !firstDisplay ? Double(self.index) * 0.04 : 0)) + .animation(Animation.spring().delay(self.touchLocation < 0 || !didCellAppear ? Double(self.index) * 0.04 : 0)) } } @@ -54,17 +39,17 @@ struct BarChartCell_Previews: PreviewProvider { static var previews: some View { Group { Group { - BarChartCell(value: 0, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat()) + BarChartCell(value: 0, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat()) - BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat()) - BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat()) - BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat()) + BarChartCell(value: 0.5, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat()) + BarChartCell(value: 0.75, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat()) + BarChartCell(value: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat()) } Group { - BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat()) - BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat()) - BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat()) + BarChartCell(value: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat()) + BarChartCell(value: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat()) + BarChartCell(value: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat()) }.environment(\.colorScheme, .dark) } } diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartCellShape.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartCellShape.swift new file mode 100644 index 00000000..5cf99839 --- /dev/null +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartCellShape.swift @@ -0,0 +1,44 @@ +import SwiftUI + +struct BarChartCellShape: Shape, Animatable { + var value: Double + var cornerRadius: CGFloat = 6.0 + var animatableData: CGFloat { + get { CGFloat(value) } + set { value = Double(newValue) } + } + + func path(in rect: CGRect) -> Path { + let adjustedOriginY = rect.height - (rect.height * CGFloat(value)) + var path = Path() + path.move(to: CGPoint(x: 0.0 , y: rect.height)) + path.addLine(to: CGPoint(x: 0.0, y: adjustedOriginY + cornerRadius)) + path.addArc(center: CGPoint(x: cornerRadius, y: adjustedOriginY + cornerRadius), + radius: cornerRadius, + startAngle: Angle(radians: Double.pi), + endAngle: Angle(radians: -Double.pi/2), + clockwise: false) + path.addLine(to: CGPoint(x: rect.width - cornerRadius, y: adjustedOriginY)) + path.addArc(center: CGPoint(x: rect.width - cornerRadius, y: adjustedOriginY + cornerRadius), + radius: cornerRadius, + startAngle: Angle(radians: -Double.pi/2), + endAngle: Angle(radians: 0), + clockwise: false) + path.addLine(to: CGPoint(x: rect.width, y: rect.height)) + path.closeSubpath() + + return path + } +} + +struct BarChartCellShape_Previews: PreviewProvider { + static var previews: some View { + Group { + BarChartCellShape(value: 0.75) + .fill(Color.red) + + BarChartCellShape(value: 0.3) + .fill(Color.blue) + } + } +} diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift index 366a3319..5cfd64b9 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift @@ -6,10 +6,6 @@ public struct BarChartRow: View { @ObservedObject var chartData: ChartData @State private var touchLocation: CGFloat = -1.0 - enum Constant { - static let spacing: CGFloat = 16.0 - } - var style: ChartStyle var maxValue: Double { @@ -27,20 +23,18 @@ public struct BarChartRow: View { public var body: some View { GeometryReader { geometry in HStack(alignment: .bottom, - spacing: (geometry.frame(in: .local).width - Constant.spacing) / CGFloat(self.chartData.data.count * 3)) { - ForEach(0.. Double { - return Double(chartData.points[index])/Double(maxValue) - } - /// Size to scale the touch indicator /// - Parameters: /// - touchLocation: fraction of width where touch is happening @@ -87,3 +74,11 @@ public struct BarChartRow: View { return self.chartData.points[index] } } + +struct BarChartRow_Previews: PreviewProvider { + static let chartData = ChartData([6, 2, 5, 8, 6]) + static let chartStyle = ChartStyle(backgroundColor: .white, foregroundColor: .orangeBright) + static var previews: some View { + BarChartRow(chartData: chartData, style: chartStyle) + } +} diff --git a/Sources/SwiftUICharts/Charts/LineChart/Line.swift b/Sources/SwiftUICharts/Charts/LineChart/Line.swift index 942342de..0e9fa34f 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/Line.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/Line.swift @@ -3,57 +3,16 @@ import SwiftUI /// A single line of data, a view in a `LineChart` public struct Line: View { @EnvironmentObject var chartValue: ChartValue - @State private var frame: CGRect = .zero @ObservedObject var chartData: ChartData var style: ChartStyle @State private var showIndicator: Bool = false @State private var touchLocation: CGPoint = .zero - @State private var showFull: Bool = false @State private var showBackground: Bool = true - var curvedLines: Bool = true - - /// Step for plotting through data - /// - Returns: X and Y delta between each data point based on data and view's frame - var step: CGPoint { - return CGPoint.getStep(frame: frame, data: chartData.points) - } - - /// Path of line graph - /// - Returns: A path for stroking representing the data, either curved or jagged. - var path: Path { - let points = chartData.points - - if curvedLines { - return Path.quadCurvedPathWithPoints(points: points, - step: step, - globalOffset: nil) - } - - return Path.linePathWithPoints(points: points, step: step) - } - - /// Path of linegraph, but also closed at the bottom side - /// - Returns: A path for filling representing the data, either curved or jagged - var closedPath: Path { - let points = chartData.points - - if curvedLines { - return Path.quadClosedCurvedPathWithPoints(points: points, - step: step, - globalOffset: nil) - } - - return Path.closedLinePathWithPoints(points: points, step: step) - } + @State private var didCellAppear: Bool = false - // see https://stackoverflow.com/a/62370919 - // This lets geometry be recalculated when device rotates. However it doesn't cover issue of app changing - // from full screen to split view. Not possible in SwiftUI? Feedback submitted to apple FB8451194. - let orientationChanged = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification) - .makeConnectable() - .autoconnect() + var curvedLines: Bool = true /// The content and behavior of the `Line`. /// Draw the background if showing the full line (?) and the `showBackground` option is set. Above that draw the line, and then the data indicator if the graph is currently being touched. @@ -62,34 +21,35 @@ public struct Line: View { public var body: some View { GeometryReader { geometry in ZStack { - if self.showFull && self.showBackground { - self.getBackgroundPathView() - } - self.getLinePathView() - if self.showIndicator { - IndicatorPoint() - .position(self.getClosestPointOnPath(touchLocation: self.touchLocation)) - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + if self.didCellAppear && self.showBackground { + LineBackgroundShapeView(chartData: chartData, + geometry: geometry, + style: style) } + LineShapeView(chartData: chartData, + geometry: geometry, + style: style, + trimTo: didCellAppear ? 1.0 : 0.0) + .animation(.easeIn) +// if self.showIndicator { +// IndicatorPoint() +// .position(self.getClosestPointOnPath(touchLocation: self.touchLocation)) +// .rotationEffect(.degrees(180), anchor: .center) +// .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) +// } } .onAppear { - self.frame = geometry.frame(in: .local) - + didCellAppear = true + } + .onDisappear() { + didCellAppear = false } - .onReceive(orientationChanged) { _ in - // When we receive notification here, the geometry is still the old value - // so delay evaluation to get the new frame! - DispatchQueue.main.async { - self.frame = geometry.frame(in: .local) // recalculate layout with new frame - } - } .gesture(DragGesture() .onChanged({ value in self.touchLocation = value.location self.showIndicator = true - self.getClosestDataPoint(point: self.getClosestPointOnPath(touchLocation: value.location)) +// self.getClosestDataPoint(point: self.getClosestPointOnPath(touchLocation: value.location)) self.chartValue.interactionInProgress = true }) .onEnded({ value in @@ -104,80 +64,37 @@ public struct Line: View { // MARK: - Private functions -extension Line { - - /// Calculate point closest to where the user touched - /// - Parameter touchLocation: location in view where touched - /// - Returns: `CGPoint` of data point on chart - private func getClosestPointOnPath(touchLocation: CGPoint) -> CGPoint { - let closest = self.path.point(to: touchLocation.x) - return closest - } +//extension Line { +// /// Calculate point closest to where the user touched +// /// - Parameter touchLocation: location in view where touched +// /// - Returns: `CGPoint` of data point on chart +// private func getClosestPointOnPath(touchLocation: CGPoint) -> CGPoint { +// let closest = self.path.point(to: touchLocation.x) +// return closest +// } +// +// /// Figure out where closest touch point was +// /// - Parameter point: location of data point on graph, near touch location +// private func getClosestDataPoint(point: CGPoint) { +// let index = Int(round((point.x)/step.x)) +// if (index >= 0 && index < self.chartData.data.count){ +// self.chartValue.currentValue = self.chartData.points[index] +// } +// } +//} - /// Figure out where closest touch point was - /// - Parameter point: location of data point on graph, near touch location - private func getClosestDataPoint(point: CGPoint) { - let index = Int(round((point.x)/step.x)) - if (index >= 0 && index < self.chartData.data.count){ - self.chartValue.currentValue = self.chartData.points[index] - } - } +struct Line_Previews: PreviewProvider { + /// Predefined style, black over white, for preview + static let blackLineStyle = ChartStyle(backgroundColor: ColorGradient(.white), foregroundColor: ColorGradient(.black)) - /// Get the view representing the filled in background below the chart, filled with the foreground color's gradient - /// - /// TODO: explain rotations - /// - Returns: SwiftUI `View` - private func getBackgroundPathView() -> some View { - self.closedPath - .fill(LinearGradient(gradient: Gradient(colors: [ - style.foregroundColor.first?.startColor ?? .white, - style.foregroundColor.first?.endColor ?? .white, - .clear]), - startPoint: .bottom, - endPoint: .top)) - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) - .opacity(0.2) - .transition(.opacity) - .animation(.easeIn(duration: 1.6)) - } + /// Predefined style red over white, for preview + static let redLineStyle = ChartStyle(backgroundColor: .whiteBlack, foregroundColor: ColorGradient(.red)) - /// Get the view representing the line stroked in the `foregroundColor` - /// - /// TODO: Explain how `showFull` works - /// TODO: explain rotations - /// - Returns: SwiftUI `View` - private func getLinePathView() -> some View { - self.path - .trim(from: 0, to: self.showFull ? 1:0) - .stroke(LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient, - startPoint: .leading, - endPoint: .trailing), - style: StrokeStyle(lineWidth: 3, lineJoin: .round)) - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) - .animation(Animation.easeOut(duration: 1.2)) - .onAppear { - self.showFull = true - } - .onDisappear { - self.showFull = false - } - .drawingGroup() - } -} - -struct Line_Previews: PreviewProvider { static var previews: some View { Group { - Line(chartData: ChartData([8, 23, 32, 7, 23, 43]), style: blackLineStyle) + Line(chartData: ChartData([8, 23, 32, 7, 23, -4]), style: blackLineStyle) Line(chartData: ChartData([8, 23, 32, 7, 23, 43]), style: redLineStyle) } } } -/// Predefined style, black over white, for preview -private let blackLineStyle = ChartStyle(backgroundColor: ColorGradient(.white), foregroundColor: ColorGradient(.black)) - -/// Predefined stylem red over white, for preview -private let redLineStyle = ChartStyle(backgroundColor: .whiteBlack, foregroundColor: ColorGradient(.red)) diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShape.swift b/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShape.swift new file mode 100644 index 00000000..06b02811 --- /dev/null +++ b/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShape.swift @@ -0,0 +1,31 @@ +import SwiftUI + +struct LineBackgroundShape: Shape { + var data: [Double] + func path(in rect: CGRect) -> Path { + let path = Path.quadClosedCurvedPathWithPoints(points: data, step: CGPoint(x: 1.0, y: 1.0)) + return path + } +} + +struct LineBackgroundShape_Previews: PreviewProvider { + static var previews: some View { + Group { + GeometryReader { geometry in + LineBackgroundShape(data: [0, 0.5, 0.8, 0.6, 1]) + .transform(CGAffineTransform(scaleX: geometry.size.width / 4.0, y: geometry.size.height)) + .fill(Color.red) + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + } + GeometryReader { geometry in + LineBackgroundShape(data: [0, -0.5, 0.8, -0.6, 1]) + .transform(CGAffineTransform(scaleX: geometry.size.width / 4.0, y: geometry.size.height / 1.6)) + .fill(Color.blue) + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + } + } + } +} + diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShapeView.swift b/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShapeView.swift new file mode 100644 index 00000000..25ba8214 --- /dev/null +++ b/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShapeView.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct LineBackgroundShapeView: View { + var chartData: ChartData + var geometry: GeometryProxy + var style: ChartStyle + + var body: some View { + LineBackgroundShape(data: chartData.normalisedPoints) + .transform(CGAffineTransform(scaleX: geometry.size.width / CGFloat(chartData.normalisedPoints.count - 1), + y: geometry.size.height / CGFloat(chartData.normalisedRange))) + .fill(LinearGradient(gradient: Gradient(colors: [style.foregroundColor.first?.startColor ?? .white, + style.backgroundColor.startColor]), + startPoint: .bottom, + endPoint: .top)) + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + } +} diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineShape.swift b/Sources/SwiftUICharts/Charts/LineChart/LineShape.swift new file mode 100644 index 00000000..64fff658 --- /dev/null +++ b/Sources/SwiftUICharts/Charts/LineChart/LineShape.swift @@ -0,0 +1,30 @@ +import SwiftUI + +struct LineShape: Shape { + var data: [Double] + func path(in rect: CGRect) -> Path { + let path = Path.quadCurvedPathWithPoints(points: data, step: CGPoint(x: 1.0, y: 1.0)) + return path + } +} + +struct LineShape_Previews: PreviewProvider { + static var previews: some View { + Group { + GeometryReader { geometry in + LineShape(data: [0, 0.5, 0.8, 0.6, 1]) + .transform(CGAffineTransform(scaleX: geometry.size.width / 4.0, y: geometry.size.height)) + .stroke(Color.red) + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + } + GeometryReader { geometry in + LineShape(data: [0, -0.5, 0.8, -0.6, 1]) + .transform(CGAffineTransform(scaleX: geometry.size.width / 4.0, y: geometry.size.height / 1.6)) + .stroke(Color.blue) + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + } + } + } +} diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineShapeView.swift b/Sources/SwiftUICharts/Charts/LineChart/LineShapeView.swift new file mode 100644 index 00000000..d7143932 --- /dev/null +++ b/Sources/SwiftUICharts/Charts/LineChart/LineShapeView.swift @@ -0,0 +1,26 @@ +import SwiftUI + +struct LineShapeView: View, Animatable { + var chartData: ChartData + var geometry: GeometryProxy + var style: ChartStyle + var trimTo: Double = 0 + + var animatableData: CGFloat { + get { CGFloat(trimTo) } + set { trimTo = Double(newValue) } + } + + var body: some View { + LineShape(data: chartData.normalisedPoints) + .trim(from: 0, to: CGFloat(trimTo)) + .transform(CGAffineTransform(scaleX: geometry.size.width / CGFloat(chartData.normalisedPoints.count - 1), + y: geometry.size.height / CGFloat(chartData.normalisedRange))) + .stroke(LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient, + startPoint: .leading, + endPoint: .trailing), + style: StrokeStyle(lineWidth: 3, lineJoin: .round)) + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + } +} diff --git a/Tests/SwiftUIChartsTests/ArrayExtensionTests.swift b/Tests/SwiftUIChartsTests/ArrayExtensionTests.swift index ad77114c..f76c00e2 100644 --- a/Tests/SwiftUIChartsTests/ArrayExtensionTests.swift +++ b/Tests/SwiftUIChartsTests/ArrayExtensionTests.swift @@ -1,10 +1,3 @@ -// -// File.swift -// -// -// Created by Nicolas Savoini on 2020-05-25. -// - @testable import SwiftUICharts import XCTest From caa75ecbc0d68f2b50d714909566406f80214186 Mon Sep 17 00:00:00 2001 From: Andras Samu Date: Wed, 11 Aug 2021 22:01:43 +0200 Subject: [PATCH 24/29] feat: add linechart interaction point (#202) * feat: add linechart interaction point * feat: add ability to show current data point on linechart --- .../xcschemes/xcschememanagement.plist | 14 +++++ .../Base/CardView/CardView.swift | 4 +- .../SwiftUICharts/Base/Chart/ChartData.swift | 3 +- .../Base/Extensions/CGPoint+Extension.swift | 6 ++ .../SwiftUICharts/Base/Grid/ChartGrid.swift | 2 +- .../SwiftUICharts/Charts/LineChart/Line.swift | 57 +++++++++++-------- 6 files changed, 59 insertions(+), 27 deletions(-) create mode 100644 .swiftpm/xcode/xcuserdata/samuandras.xcuserdatad/xcschemes/xcschememanagement.plist diff --git a/.swiftpm/xcode/xcuserdata/samuandras.xcuserdatad/xcschemes/xcschememanagement.plist b/.swiftpm/xcode/xcuserdata/samuandras.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 00000000..a0f26bbb --- /dev/null +++ b/.swiftpm/xcode/xcuserdata/samuandras.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + SwiftUICharts.xcscheme_^#shared#^_ + + orderHint + 2 + + + + diff --git a/Sources/SwiftUICharts/Base/CardView/CardView.swift b/Sources/SwiftUICharts/Base/CardView/CardView.swift index 9109a2b3..e5d7eb3a 100644 --- a/Sources/SwiftUICharts/Base/CardView/CardView.swift +++ b/Sources/SwiftUICharts/Base/CardView/CardView.swift @@ -26,9 +26,9 @@ public struct CardView: View, ChartBase { if showShadow { RoundedRectangle(cornerRadius: 20) .fill(Color.white) - .shadow(color: Color.gray, radius: 8) + .shadow(color: Color(white: 0.9, opacity: 1), radius: 8) } - VStack { + VStack (alignment: .leading) { self.content() } .clipShape(RoundedRectangle(cornerRadius: showShadow ? 20 : 0)) diff --git a/Sources/SwiftUICharts/Base/Chart/ChartData.swift b/Sources/SwiftUICharts/Base/Chart/ChartData.swift index b598975c..17b79ee8 100644 --- a/Sources/SwiftUICharts/Base/Chart/ChartData.swift +++ b/Sources/SwiftUICharts/Base/Chart/ChartData.swift @@ -13,7 +13,8 @@ public class ChartData: ObservableObject { } var normalisedPoints: [Double] { - points.map { $0 / (points.max() ?? 1.0) } + let absolutePoints = points.map { abs($0) } + return points.map { $0 / (absolutePoints.max() ?? 1.0) } } var normalisedRange: Double { diff --git a/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift index 363cd2c3..31c401d9 100644 --- a/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift @@ -38,4 +38,10 @@ extension CGPoint { return CGPoint(x: stepWidth, y: stepHeight) } + + func denormalize(with geometry: GeometryProxy) -> CGPoint { + let width = geometry.frame(in: .local).width + let height = geometry.frame(in: .local).height + return CGPoint(x: self.x * width, y: self.y * height) + } } diff --git a/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift b/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift index ecaf270b..eb48a98e 100644 --- a/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift +++ b/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift @@ -46,7 +46,7 @@ struct DashedLine: View { var body: some View { GeometryReader { geometry in line(frame: geometry.frame(in: .local)) - .stroke(Color(white: 0.3), style: StrokeStyle(lineWidth: 1, lineCap: .round, dash: [5, 10])) + .stroke(Color(white: 0.85), style: StrokeStyle(lineWidth: 1, lineCap: .round, dash: [5, 10])) } } } diff --git a/Sources/SwiftUICharts/Charts/LineChart/Line.swift b/Sources/SwiftUICharts/Charts/LineChart/Line.swift index 0e9fa34f..107367fa 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/Line.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/Line.swift @@ -13,6 +13,10 @@ public struct Line: View { @State private var didCellAppear: Bool = false var curvedLines: Bool = true + var path: Path { + Path.quadCurvedPathWithPoints(points: chartData.normalisedPoints, + step: CGPoint(x: 1.0, y: 1.0)) + } /// The content and behavior of the `Line`. /// Draw the background if showing the full line (?) and the `showBackground` option is set. Above that draw the line, and then the data indicator if the graph is currently being touched. @@ -31,12 +35,13 @@ public struct Line: View { style: style, trimTo: didCellAppear ? 1.0 : 0.0) .animation(.easeIn) -// if self.showIndicator { -// IndicatorPoint() -// .position(self.getClosestPointOnPath(touchLocation: self.touchLocation)) -// .rotationEffect(.degrees(180), anchor: .center) -// .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) -// } + if self.showIndicator { + IndicatorPoint() + .position(self.getClosestPointOnPath(geometry: geometry, + touchLocation: self.touchLocation)) + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + } } .onAppear { didCellAppear = true @@ -49,7 +54,7 @@ public struct Line: View { .onChanged({ value in self.touchLocation = value.location self.showIndicator = true -// self.getClosestDataPoint(point: self.getClosestPointOnPath(touchLocation: value.location)) + self.getClosestDataPoint(geometry: geometry, touchLocation: value.location) self.chartValue.interactionInProgress = true }) .onEnded({ value in @@ -64,24 +69,30 @@ public struct Line: View { // MARK: - Private functions -//extension Line { -// /// Calculate point closest to where the user touched -// /// - Parameter touchLocation: location in view where touched -// /// - Returns: `CGPoint` of data point on chart -// private func getClosestPointOnPath(touchLocation: CGPoint) -> CGPoint { -// let closest = self.path.point(to: touchLocation.x) -// return closest -// } -// +extension Line { + /// Calculate point closest to where the user touched + /// - Parameter touchLocation: location in view where touched + /// - Returns: `CGPoint` of data point on chart + private func getClosestPointOnPath(geometry: GeometryProxy, touchLocation: CGPoint) -> CGPoint { + let geometryWidth = geometry.frame(in: .local).width + let normalisedTouchLocationX = (touchLocation.x / geometryWidth) * CGFloat(chartData.normalisedPoints.count - 1) + let closest = self.path.point(to: normalisedTouchLocationX) + var denormClosest = closest.denormalize(with: geometry) + denormClosest.x = denormClosest.x / CGFloat(chartData.normalisedPoints.count - 1) + denormClosest.y = denormClosest.y / CGFloat(chartData.normalisedRange) + return denormClosest + } + // /// Figure out where closest touch point was // /// - Parameter point: location of data point on graph, near touch location -// private func getClosestDataPoint(point: CGPoint) { -// let index = Int(round((point.x)/step.x)) -// if (index >= 0 && index < self.chartData.data.count){ -// self.chartValue.currentValue = self.chartData.points[index] -// } -// } -//} + private func getClosestDataPoint(geometry: GeometryProxy, touchLocation: CGPoint) { + let geometryWidth = geometry.frame(in: .local).width + let index = Int(round((touchLocation.x / geometryWidth) * CGFloat(chartData.points.count - 1))) + if (index >= 0 && index < self.chartData.data.count){ + self.chartValue.currentValue = self.chartData.points[index] + } + } +} struct Line_Previews: PreviewProvider { /// Predefined style, black over white, for preview From bd29afc4c9eaa93e540b6e93f19447d6d1fca835 Mon Sep 17 00:00:00 2001 From: Andras Samu Date: Sat, 3 Sep 2022 18:55:59 +0200 Subject: [PATCH 25/29] fix: BarChartCellShape to handle negative numbers correctly (#250) --- .../Charts/BarChart/BarChartCellShape.swift | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartCellShape.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartCellShape.swift index 5cf99839..268f4229 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartCellShape.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartCellShape.swift @@ -3,6 +3,7 @@ import SwiftUI struct BarChartCellShape: Shape, Animatable { var value: Double var cornerRadius: CGFloat = 6.0 + var animatableData: CGFloat { get { CGFloat(value) } set { value = Double(newValue) } @@ -16,14 +17,14 @@ struct BarChartCellShape: Shape, Animatable { path.addArc(center: CGPoint(x: cornerRadius, y: adjustedOriginY + cornerRadius), radius: cornerRadius, startAngle: Angle(radians: Double.pi), - endAngle: Angle(radians: -Double.pi/2), - clockwise: false) - path.addLine(to: CGPoint(x: rect.width - cornerRadius, y: adjustedOriginY)) + endAngle: Angle(radians: value < 0 ? Double.pi/2 : -Double.pi/2), + clockwise: value < 0 ? true : false) + path.addLine(to: CGPoint(x: rect.width - cornerRadius, y: value < 0 ? adjustedOriginY + 2 * cornerRadius : adjustedOriginY)) path.addArc(center: CGPoint(x: rect.width - cornerRadius, y: adjustedOriginY + cornerRadius), radius: cornerRadius, - startAngle: Angle(radians: -Double.pi/2), + startAngle: Angle(radians: value < 0 ? Double.pi/2 : -Double.pi/2), endAngle: Angle(radians: 0), - clockwise: false) + clockwise: value < 0 ? true : false) path.addLine(to: CGPoint(x: rect.width, y: rect.height)) path.closeSubpath() @@ -39,6 +40,10 @@ struct BarChartCellShape_Previews: PreviewProvider { BarChartCellShape(value: 0.3) .fill(Color.blue) + + BarChartCellShape(value: -0.3) + .fill(Color.blue) + .offset(x: 0, y: -600) } } } From d7e9802debff03bc7fde8601369a8302bcd5e3bb Mon Sep 17 00:00:00 2001 From: Andras Samu Date: Sat, 3 Sep 2022 19:01:01 +0200 Subject: [PATCH 26/29] fix: remove UIColors which caused CI build errors (#251) --- .../SwiftUICharts/Base/Label/ChartLabel.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/SwiftUICharts/Base/Label/ChartLabel.swift b/Sources/SwiftUICharts/Base/Label/ChartLabel.swift index 96a86303..fb1a8909 100644 --- a/Sources/SwiftUICharts/Base/Label/ChartLabel.swift +++ b/Sources/SwiftUICharts/Base/Label/ChartLabel.swift @@ -39,13 +39,13 @@ public struct ChartLabel: View { private var labelPadding: EdgeInsets { switch labelType { case .title: - return EdgeInsets(top: 16.0, leading: 8.0, bottom: 0.0, trailing: 8.0) + return EdgeInsets(top: 16.0, leading: 0, bottom: 0.0, trailing: 8.0) case .legend: - return EdgeInsets(top: 4.0, leading: 8.0, bottom: 0.0, trailing: 8.0) + return EdgeInsets(top: 4.0, leading: 0, bottom: 0.0, trailing: 8.0) case .subTitle: - return EdgeInsets(top: 8.0, leading: 8.0, bottom: 0.0, trailing: 8.0) + return EdgeInsets(top: 8.0, leading: 0, bottom: 0.0, trailing: 8.0) case .largeTitle: - return EdgeInsets(top: 24.0, leading: 8.0, bottom: 0.0, trailing: 8.0) + return EdgeInsets(top: 24.0, leading: 0, bottom: 0.0, trailing: 8.0) case .custom(_, let padding, _): return padding } @@ -59,13 +59,13 @@ public struct ChartLabel: View { private var labelColor: Color { switch labelType { case .title: - return Color(UIColor.label) + return Color.primary case .legend: - return Color(UIColor.secondaryLabel) + return Color.secondary case .subTitle: - return Color(UIColor.label) + return Color.primary case .largeTitle: - return Color(UIColor.label) + return Color.primary case .custom(_, _, let color): return color } From ebaaf81d1946d2ccbbbaf58ac6bbe92b928f9a66 Mon Sep 17 00:00:00 2001 From: Andras Samu Date: Mon, 24 Oct 2022 16:07:51 +0200 Subject: [PATCH 27/29] =?UTF-8?q?feat:=20new=20protocol=20for=20chained=20?= =?UTF-8?q?functions,=20and=20added=20support=20for=20expli=E2=80=A6=20(#2?= =?UTF-8?q?52)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: new protocol for chained functions, and added support for explicit Y ranges. X coming as well * feat: add new axis interface (#253) --- .../SwiftUICharts/Base/Axis/AxisLabels.swift | 98 +++++++ .../Axis/Extension/AxisLabels+Extension.swift | 57 ++++ .../Base/Axis/Model/AxisLabelsPosition.swift | 11 + .../Base/Axis/Model/AxisLabelsStyle.swift | 11 + .../Base/Axis/Model/AxisLablesData.swift | 10 + .../SwiftUICharts/Base/Chart/ChartBase.swift | 2 +- .../SwiftUICharts/Base/Chart/ChartData.swift | 55 +++- .../Base/Common/ViewGeometry.swift | 10 + .../Base/Common/ViewPreferenceKey.swift | 15 + .../Base/Common/ViewSizeData.swift | 14 + .../Base/Extensions/Array+Extension.swift | 7 + .../Base/Extensions/ChartBase+Extension.swift | 26 +- .../Base/Extensions/Path+QuadCurve.swift | 256 +++++++----------- .../Base/Extensions/Range+Extension.swift | 7 + .../Base/Extensions/Shape+Extension.swift | 17 ++ .../Base/Extensions/View+Extension.swift | 6 + .../SwiftUICharts/Base/Grid/ChartGrid.swift | 51 +--- .../Base/Grid/ChartGridBaseShape.swift | 18 ++ .../Base/Grid/ChartGridShape.swift | 28 ++ .../Base/Grid/Extension/Grid+Extension.swift | 31 +++ .../Base/Grid/Model/GridOptions.swift | 14 + .../SwiftUICharts/Base/Style/ChartStyle.swift | 20 -- .../Base/Style/ColorGradient.swift | 14 - Sources/SwiftUICharts/Base/Style/Colors.swift | 2 - .../Charts/BarChart/BarChart.swift | 9 +- .../Charts/BarChart/BarChartCell.swift | 4 - .../Charts/BarChart/BarChartRow.swift | 14 - .../Extension/LineChart+Extension.swift | 24 ++ .../Charts/LineChart/IndicatorPoint.swift | 4 - .../SwiftUICharts/Charts/LineChart/Line.swift | 64 +++-- .../LineChart/LineBackgroundShape.swift | 16 +- .../LineChart/LineBackgroundShapeView.swift | 13 +- .../Charts/LineChart/LineChart.swift | 13 +- .../Charts/LineChart/LineShape.swift | 32 +-- .../Charts/LineChart/LineShapeView.swift | 77 +++++- .../Charts/LineChart/MarkerShape.swift | 23 ++ .../LineChart/Model/LineChartProperties.swift | 13 + .../Charts/LineChart/Model/LineStyle.swift | 6 + .../Charts/PieChart/PieChart.swift | 5 +- .../Charts/RingsChart/RingsChart.swift | 12 +- 40 files changed, 730 insertions(+), 379 deletions(-) create mode 100644 Sources/SwiftUICharts/Base/Axis/AxisLabels.swift create mode 100644 Sources/SwiftUICharts/Base/Axis/Extension/AxisLabels+Extension.swift create mode 100644 Sources/SwiftUICharts/Base/Axis/Model/AxisLabelsPosition.swift create mode 100644 Sources/SwiftUICharts/Base/Axis/Model/AxisLabelsStyle.swift create mode 100644 Sources/SwiftUICharts/Base/Axis/Model/AxisLablesData.swift create mode 100644 Sources/SwiftUICharts/Base/Common/ViewGeometry.swift create mode 100644 Sources/SwiftUICharts/Base/Common/ViewPreferenceKey.swift create mode 100644 Sources/SwiftUICharts/Base/Common/ViewSizeData.swift create mode 100644 Sources/SwiftUICharts/Base/Extensions/Range+Extension.swift create mode 100644 Sources/SwiftUICharts/Base/Extensions/Shape+Extension.swift create mode 100644 Sources/SwiftUICharts/Base/Grid/ChartGridBaseShape.swift create mode 100644 Sources/SwiftUICharts/Base/Grid/ChartGridShape.swift create mode 100644 Sources/SwiftUICharts/Base/Grid/Extension/Grid+Extension.swift create mode 100644 Sources/SwiftUICharts/Base/Grid/Model/GridOptions.swift create mode 100644 Sources/SwiftUICharts/Charts/LineChart/Extension/LineChart+Extension.swift create mode 100644 Sources/SwiftUICharts/Charts/LineChart/MarkerShape.swift create mode 100644 Sources/SwiftUICharts/Charts/LineChart/Model/LineChartProperties.swift create mode 100644 Sources/SwiftUICharts/Charts/LineChart/Model/LineStyle.swift diff --git a/Sources/SwiftUICharts/Base/Axis/AxisLabels.swift b/Sources/SwiftUICharts/Base/Axis/AxisLabels.swift new file mode 100644 index 00000000..0efaa897 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Axis/AxisLabels.swift @@ -0,0 +1,98 @@ +import SwiftUI + +public struct AxisLabels: View { + struct YAxisViewKey: ViewPreferenceKey { } + struct ChartViewKey: ViewPreferenceKey { } + + var axisLabelsData = AxisLabelsData() + var axisLabelsStyle = AxisLabelsStyle() + + @State private var yAxisWidth: CGFloat = 25 + @State private var chartWidth: CGFloat = 0 + @State private var chartHeight: CGFloat = 0 + + let content: () -> Content + + public init(@ViewBuilder content: @escaping () -> Content) { + self.content = content + } + + var yAxis: some View { + VStack(spacing: 0.0) { + ForEach(Array(axisLabelsData.axisYLabels.reversed().enumerated()), id: \.element) { index, axisYData in + Text(axisYData) + .font(axisLabelsStyle.axisFont) + .foregroundColor(axisLabelsStyle.axisFontColor) + .frame(height: getYHeight(index: index, + chartHeight: chartHeight, + count: axisLabelsData.axisYLabels.count), + alignment: getYAlignment(index: index, count: axisLabelsData.axisYLabels.count)) + } + } + .padding([.leading, .trailing], 4.0) + .background(ViewGeometry()) + .onPreferenceChange(YAxisViewKey.self) { value in + yAxisWidth = value.first?.size.width ?? 0.0 + } + } + + func xAxis(chartWidth: CGFloat) -> some View { + HStack(spacing: 0.0) { + ForEach(Array(axisLabelsData.axisXLabels.enumerated()), id: \.element) { index, axisXData in + Text(axisXData) + .font(axisLabelsStyle.axisFont) + .foregroundColor(axisLabelsStyle.axisFontColor) + .frame(width: chartWidth / CGFloat(axisLabelsData.axisXLabels.count - 1)) + } + } + .frame(height: 24.0, alignment: .top) + } + + var chart: some View { + self.content() + .background(ViewGeometry()) + .onPreferenceChange(ChartViewKey.self) { value in + chartWidth = value.first?.size.width ?? 0.0 + chartHeight = value.first?.size.height ?? 0.0 + } + } + + public var body: some View { + VStack(spacing: 0.0) { + HStack { + if axisLabelsStyle.axisLabelsYPosition == .leading { + yAxis + } else { + Spacer(minLength: yAxisWidth) + } + chart + if axisLabelsStyle.axisLabelsYPosition == .leading { + Spacer(minLength: yAxisWidth) + } else { + yAxis + } + } + xAxis(chartWidth: chartWidth) + } + } + + private func getYHeight(index: Int, chartHeight: CGFloat, count: Int) -> CGFloat { + if index == 0 || index == count - 1 { + return chartHeight / (CGFloat(count - 1) * 2) + 10 + } + + return chartHeight / CGFloat(count - 1) + } + + private func getYAlignment(index: Int, count: Int) -> Alignment { + if index == 0 { + return .top + } + + if index == count - 1 { + return .bottom + } + + return .center + } +} diff --git a/Sources/SwiftUICharts/Base/Axis/Extension/AxisLabels+Extension.swift b/Sources/SwiftUICharts/Base/Axis/Extension/AxisLabels+Extension.swift new file mode 100644 index 00000000..7698ac4e --- /dev/null +++ b/Sources/SwiftUICharts/Base/Axis/Extension/AxisLabels+Extension.swift @@ -0,0 +1,57 @@ +import SwiftUI + +extension AxisLabels { + public func setAxisYLabels(_ labels: [String], + position: AxisLabelsYPosition = .leading) -> AxisLabels { + self.axisLabelsData.axisYLabels = labels + self.axisLabelsStyle.axisLabelsYPosition = position + return self + } + + public func setAxisXLabels(_ labels: [String]) -> AxisLabels { + self.axisLabelsData.axisXLabels = labels + return self + } + + public func setAxisYLabels(_ labels: [(Double, String)], + range: ClosedRange, + position: AxisLabelsYPosition = .leading) -> AxisLabels { + let overreach = range.overreach + 1 + var labelArray = [String](repeating: "", count: overreach) + labels.forEach { + let index = Int($0.0) - range.lowerBound + if labelArray[safe: index] != nil { + labelArray[index] = $0.1 + } + } + + self.axisLabelsData.axisYLabels = labelArray + self.axisLabelsStyle.axisLabelsYPosition = position + + return self + } + + public func setAxisXLabels(_ labels: [(Double, String)], range: ClosedRange) -> AxisLabels { + let overreach = range.overreach + 1 + var labelArray = [String](repeating: "", count: overreach) + labels.forEach { + let index = Int($0.0) - range.lowerBound + if labelArray[safe: index] != nil { + labelArray[index] = $0.1 + } + } + + self.axisLabelsData.axisXLabels = labelArray + return self + } + + public func setColor(_ color: Color) -> AxisLabels { + self.axisLabelsStyle.axisFontColor = color + return self + } + + public func setFont(_ font: Font) -> AxisLabels { + self.axisLabelsStyle.axisFont = font + return self + } +} diff --git a/Sources/SwiftUICharts/Base/Axis/Model/AxisLabelsPosition.swift b/Sources/SwiftUICharts/Base/Axis/Model/AxisLabelsPosition.swift new file mode 100644 index 00000000..66735d7b --- /dev/null +++ b/Sources/SwiftUICharts/Base/Axis/Model/AxisLabelsPosition.swift @@ -0,0 +1,11 @@ +import Foundation + +public enum AxisLabelsYPosition { + case leading + case trailing +} + +public enum AxisLabelsXPosition { + case top + case bottom +} diff --git a/Sources/SwiftUICharts/Base/Axis/Model/AxisLabelsStyle.swift b/Sources/SwiftUICharts/Base/Axis/Model/AxisLabelsStyle.swift new file mode 100644 index 00000000..58221426 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Axis/Model/AxisLabelsStyle.swift @@ -0,0 +1,11 @@ +import SwiftUI + +public final class AxisLabelsStyle: ObservableObject { + @Published public var axisFont: Font = .callout + @Published public var axisFontColor: Color = .primary + @Published var axisLabelsYPosition: AxisLabelsYPosition = .leading + @Published var axisLabelsXPosition: AxisLabelsXPosition = .bottom + public init() { + // no-op + } +} diff --git a/Sources/SwiftUICharts/Base/Axis/Model/AxisLablesData.swift b/Sources/SwiftUICharts/Base/Axis/Model/AxisLablesData.swift new file mode 100644 index 00000000..f28f35ac --- /dev/null +++ b/Sources/SwiftUICharts/Base/Axis/Model/AxisLablesData.swift @@ -0,0 +1,10 @@ +import SwiftUI + +public final class AxisLabelsData: ObservableObject { + @Published public var axisYLabels: [String] = [] + @Published public var axisXLabels: [String] = [] + + public init() { + // no-op + } +} diff --git a/Sources/SwiftUICharts/Base/Chart/ChartBase.swift b/Sources/SwiftUICharts/Base/Chart/ChartBase.swift index 9cf00102..f1876dcb 100644 --- a/Sources/SwiftUICharts/Base/Chart/ChartBase.swift +++ b/Sources/SwiftUICharts/Base/Chart/ChartBase.swift @@ -1,6 +1,6 @@ import SwiftUI /// Protocol for any type of chart, to get access to underlying data -public protocol ChartBase { +public protocol ChartBase: View { var chartData: ChartData { get } } diff --git a/Sources/SwiftUICharts/Base/Chart/ChartData.swift b/Sources/SwiftUICharts/Base/Chart/ChartData.swift index 17b79ee8..f1eeec36 100644 --- a/Sources/SwiftUICharts/Base/Chart/ChartData.swift +++ b/Sources/SwiftUICharts/Base/Chart/ChartData.swift @@ -2,37 +2,70 @@ import SwiftUI /// An observable wrapper for an array of data for use in any chart public class ChartData: ObservableObject { - @Published public var data: [(String, Double)] = [] + @Published public var data: [(Double, Double)] = [] + public var rangeY: ClosedRange? + public var rangeX: ClosedRange? var points: [Double] { - data.map { $0.1 } + data.filter { rangeX?.contains($0.0) ?? true }.map { $0.1 } } - var values: [String] { - data.map { $0.0 } + var values: [Double] { + data.filter { rangeX?.contains($0.0) ?? true }.map { $0.0 } } var normalisedPoints: [Double] { let absolutePoints = points.map { abs($0) } - return points.map { $0 / (absolutePoints.max() ?? 1.0) } + var maxPoint = absolutePoints.max() + if let rangeY = rangeY { + maxPoint = Double(rangeY.overreach) + return points.map { ($0 - rangeY.lowerBound) / (maxPoint ?? 1.0) } + } + + return points.map { $0 / (maxPoint ?? 1.0) } } - var normalisedRange: Double { - (normalisedPoints.max() ?? 0.0) - (normalisedPoints.min() ?? 0.0) + var normalisedValues: [Double] { + let absoluteValues = values.map { abs($0) } + var maxValue = absoluteValues.max() + if let rangeX = rangeX { + maxValue = Double(rangeX.overreach) + return values.map { ($0 - rangeX.lowerBound) / (maxValue ?? 1.0) } + } + + return values.map { $0 / (maxValue ?? 1.0) } + } + + var normalisedData: [(Double, Double)] { + Array(zip(normalisedValues, normalisedPoints)) + } + + var normalisedYRange: Double { + return rangeY == nil ? (normalisedPoints.max() ?? 0.0) - (normalisedPoints.min() ?? 0.0) : 1 + } + + var normalisedXRange: Double { + return rangeX == nil ? (normalisedValues.max() ?? 0.0) - (normalisedValues.min() ?? 0.0) : 1 } var isInNegativeDomain: Bool { - (points.min() ?? 0.0) < 0 + if let rangeY = rangeY { + return rangeY.lowerBound < 0 + } + + return (points.min() ?? 0.0) < 0 } /// Initialize with data array /// - Parameter data: Array of `Double` - public init(_ data: [Double]) { - self.data = data.map { ("", $0) } + public init(_ data: [Double], rangeY: ClosedRange? = nil) { + self.data = data.enumerated().map{ (index, value) in (Double(index), value) } + self.rangeY = rangeY } - public init(_ data: [(String, Double)]) { + public init(_ data: [(Double, Double)], rangeY: ClosedRange? = nil) { self.data = data + self.rangeY = rangeY } public init() { diff --git a/Sources/SwiftUICharts/Base/Common/ViewGeometry.swift b/Sources/SwiftUICharts/Base/Common/ViewGeometry.swift new file mode 100644 index 00000000..ea8357f1 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Common/ViewGeometry.swift @@ -0,0 +1,10 @@ +import SwiftUI + +public struct ViewGeometry: View where T: PreferenceKey { + public var body: some View { + GeometryReader { geometry in + Color.clear + .preference(key: T.self, value: [ViewSizeData(size: geometry.size)] as! T.Value) + } + } +} diff --git a/Sources/SwiftUICharts/Base/Common/ViewPreferenceKey.swift b/Sources/SwiftUICharts/Base/Common/ViewPreferenceKey.swift new file mode 100644 index 00000000..d3c4c1f1 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Common/ViewPreferenceKey.swift @@ -0,0 +1,15 @@ +import SwiftUI + +public protocol ViewPreferenceKey: PreferenceKey { + typealias Value = [ViewSizeData] +} + +public extension ViewPreferenceKey { + static var defaultValue: [ViewSizeData] { + [] + } + + static func reduce(value: inout [ViewSizeData], nextValue: () -> [ViewSizeData]) { + value.append(contentsOf: nextValue()) + } +} diff --git a/Sources/SwiftUICharts/Base/Common/ViewSizeData.swift b/Sources/SwiftUICharts/Base/Common/ViewSizeData.swift new file mode 100644 index 00000000..9a53cec3 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Common/ViewSizeData.swift @@ -0,0 +1,14 @@ +import SwiftUI + +public struct ViewSizeData: Identifiable, Equatable, Hashable { + public let id: UUID = UUID() + public let size: CGSize + + public static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift index 874430ca..1e4bedd7 100644 --- a/Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift @@ -17,3 +17,10 @@ extension Array where Element == ColorGradient { return self[index] } } + +extension Collection { + /// Returns the element at the specified index if it is within bounds, otherwise nil. + subscript (safe index: Index) -> Element? { + return indices.contains(index) ? self[index] : nil + } +} diff --git a/Sources/SwiftUICharts/Base/Extensions/ChartBase+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/ChartBase+Extension.swift index 73873d78..96ec022a 100644 --- a/Sources/SwiftUICharts/Base/Extensions/ChartBase+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/ChartBase+Extension.swift @@ -1,21 +1,23 @@ import SwiftUI -extension View where Self: ChartBase { - - /// Set data for a chart - /// - Parameter data: array of `Double` - /// - Returns: modified `View` with data attached - public func data(_ data: [Double]) -> some View { - chartData.data = data.map { ("", $0) } +extension ChartBase { + public func data(_ data: [Double]) -> some ChartBase { + chartData.data = data.enumerated().map{ (index, value) in (Double(index), value) } return self - .environmentObject(chartData) - .environmentObject(ChartValue()) } - public func data(_ data: [(String, Double)]) -> some View { + public func data(_ data: [(Double, Double)]) -> some ChartBase { chartData.data = data return self - .environmentObject(chartData) - .environmentObject(ChartValue()) + } + + public func rangeY(_ range: ClosedRange) -> some ChartBase{ + chartData.rangeY = range + return self + } + + public func rangeX(_ range: ClosedRange) -> some ChartBase{ + chartData.rangeX = range + return self } } diff --git a/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift b/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift index 1da24afc..8085fd0c 100644 --- a/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift +++ b/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift @@ -1,47 +1,31 @@ import SwiftUI extension Path { - - /// Returns a tiny segment of path based on percentage along the path - /// - /// TODO: Explain why more than 1 gets 0 and why less than 0 gets 1 - /// - Parameter percent: fraction along data set, between 0.0 and 1.0 (underflow and overflow are handled) - /// - Returns: tiny path right around the requested fraction func trimmedPath(for percent: CGFloat) -> Path { let boundsDistance: CGFloat = 0.001 let completion: CGFloat = 1 - boundsDistance let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent) - // Start/end points centered around given percentage, but capped if right at the very end + // Start/end points centered around given percentage, but capped if right at the very end let start = pct > completion ? completion : pct - boundsDistance let end = pct > completion ? 1 : pct + boundsDistance return trimmedPath(from: start, to: end) } - /// Find the `CGPoint` for the given fraction along the path. - /// - /// This works by requesting a very tiny trimmed section of the path, then getting the center of the bounds rectangle - /// - Parameter percent: fraction along data set, between 0.0 and 1.0 (underflow and overflow are handled) - /// - Returns: a `CGPoint` representing the location of that section of the path func point(for percent: CGFloat) -> CGPoint { let path = trimmedPath(for: percent) return CGPoint(x: path.boundingRect.midX, y: path.boundingRect.midY) } - /// <#Description#> - /// - Parameter maxX: <#maxX description#> - /// - Returns: <#description#> func point(to maxX: CGFloat) -> CGPoint { let total = length let sub = length(to: maxX) let percent = sub / total return point(for: percent) } - - /// <#Description#> - /// - Returns: <#description#> - var length: CGFloat { + + var length: CGFloat { var ret: CGFloat = 0.0 var start: CGPoint? var point = CGPoint.zero @@ -73,9 +57,6 @@ extension Path { return ret } - /// <#Description#> - /// - Parameter maxX: <#maxX description#> - /// - Returns: <#description#> func length(to maxX: CGFloat) -> CGFloat { var ret: CGFloat = 0.0 var start: CGPoint? @@ -127,19 +108,13 @@ extension Path { return ret } - /// <#Description#> - /// - Parameters: - /// - points: <#points description#> - /// - step: <#step description#> - /// - globalOffset: <#globalOffset description#> - /// - Returns: <#description#> static func quadCurvedPathWithPoints(points: [Double], step: CGPoint, globalOffset: Double? = nil) -> Path { var path = Path() if points.count < 2 { return path } let offset = globalOffset ?? points.min()! -// guard let offset = points.min() else { return path } + // guard let offset = points.min() else { return path } var point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y) path.move(to: point1) for pointIndex in 1.. - /// - Parameters: - /// - points: <#points description#> - /// - step: <#step description#> - /// - globalOffset: <#globalOffset description#> - /// - Returns: <#description#> - static func quadClosedCurvedPathWithPoints(points: [Double], step: CGPoint, globalOffset: Double? = nil) -> Path { + static func quadCurvedPathWithPoints(data: [(Double, Double)], in rect: CGRect) -> Path { var path = Path() - if points.count < 2 { + if data.count < 2 { return path } - let offset = globalOffset ?? points.min()! -// guard let offset = points.min() else { return path } - path.move(to: .zero) - var point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y) + let convertedXValues = data.map { CGFloat($0.0) * rect.width } + let convertedYPoints = data.map { CGFloat($0.1) * rect.height } + + var point1 = CGPoint(x: convertedXValues[0], y: convertedYPoints[0]) + path.move(to: point1) + for pointIndex in 1.. Path { + var path = Path() + let filteredData = data.filter { $0.1 <= 1 && $0.1 >= 0 } + + if filteredData.count < 1 { + return path + } + + let convertedXValues = filteredData.map { CGFloat($0.0) * rect.width } + let convertedYPoints = filteredData.map { CGFloat($0.1) * rect.height } + + let markerSize = CGSize(width: 8, height: 8) + for pointIndex in 0.. Path { + var path = Path() + + for index in 0.. Path { + var path = Path() + if data.count < 2 { + return path + } + + let convertedXValues = data.map { CGFloat($0.0) * rect.width } + let convertedYPoints = data.map { CGFloat($0.1) * rect.height } + + path.move(to: CGPoint(x: convertedXValues[0], y: 0)) + var point1 = CGPoint(x: convertedXValues[0], y: convertedYPoints[0]) path.addLine(to: point1) - for pointIndex in 1.. - /// - Parameters: - /// - points: <#points description#> - /// - step: <#step description#> - /// - Returns: <#description#> - static func linePathWithPoints(points: [Double], step: CGPoint) -> Path { + static func linePathWithPoints(data: [(Double, Double)], in rect: CGRect) -> Path { var path = Path() - if points.count < 2 { + if data.count < 2 { return path } - guard let offset = points.min() else { - return path - } - let point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y) + + let convertedXValues = data.map { CGFloat($0.0) * rect.width } + let convertedYPoints = data.map { CGFloat($0.1) * rect.height } + + let point1 = CGPoint(x: convertedXValues[0], y: convertedYPoints[0]) path.move(to: point1) - for pointIndex in 1.. - /// - Parameters: - /// - points: <#points description#> - /// - step: <#step description#> - /// - Returns: <#description#> - static func closedLinePathWithPoints(points: [Double], step: CGPoint) -> Path { + static func closedLinePathWithPoints(data: [(Double, Double)], in rect: CGRect) -> Path { var path = Path() - if points.count < 2 { - return path - } - guard let offset = points.min() else { + if data.count < 2 { return path } - var point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y) - path.move(to: point1) - for pointIndex in 1.. - /// - Parameters: - /// - to: <#to description#> - /// - x: <#x description#> - /// - Returns: <#description#> func point(to: CGPoint, x: CGFloat) -> CGPoint { let a = (to.y - self.y) / (to.x - self.x) let y = self.y + (x - self.x) * a return CGPoint(x: x, y: y) } - /// <#Description#> - /// - Parameter to: <#to description#> - /// - Returns: <#description#> func line(to: CGPoint) -> CGFloat { dist(to: to) } - /// <#Description#> - /// - Parameters: - /// - to: <#to description#> - /// - x: <#x description#> - /// - Returns: <#description#> func line(to: CGPoint, x: CGFloat) -> CGFloat { dist(to: point(to: to, x: x)) } - /// <#Description#> - /// - Parameters: - /// - to: <#to description#> - /// - control: <#control description#> - /// - Returns: <#description#> func quadCurve(to: CGPoint, control: CGPoint) -> CGFloat { var dist: CGFloat = 0 let steps: CGFloat = 100 @@ -278,12 +289,6 @@ extension CGPoint { return dist } - /// <#Description#> - /// - Parameters: - /// - to: <#to description#> - /// - control: <#control description#> - /// - x: <#x description#> - /// - Returns: <#description#> func quadCurve(to: CGPoint, control: CGPoint, x: CGFloat) -> CGFloat { var dist: CGFloat = 0 let steps: CGFloat = 100 @@ -309,12 +314,6 @@ extension CGPoint { return dist } - /// <#Description#> - /// - Parameters: - /// - to: <#to description#> - /// - t: <#t description#> - /// - control: <#control description#> - /// - Returns: <#description#> func point(to: CGPoint, t: CGFloat, control: CGPoint) -> CGPoint { let x = CGPoint.value(x: self.x, y: to.x, t: t, c: control.x) let y = CGPoint.value(x: self.y, y: to.y, t: t, c: control.y) @@ -322,12 +321,6 @@ extension CGPoint { return CGPoint(x: x, y: y) } - /// <#Description#> - /// - Parameters: - /// - to: <#to description#> - /// - control1: <#control1 description#> - /// - control2: <#control2 description#> - /// - Returns: <#description#> func curve(to: CGPoint, control1: CGPoint, control2: CGPoint) -> CGFloat { var dist: CGFloat = 0 let steps: CGFloat = 100 @@ -345,13 +338,6 @@ extension CGPoint { return dist } - /// <#Description#> - /// - Parameters: - /// - to: <#to description#> - /// - control1: <#control1 description#> - /// - control2: <#control2 description#> - /// - x: <#x description#> - /// - Returns: <#description#> func curve(to: CGPoint, control1: CGPoint, control2: CGPoint, x: CGFloat) -> CGFloat { var dist: CGFloat = 0 let steps: CGFloat = 100 @@ -379,13 +365,6 @@ extension CGPoint { return dist } - /// <#Description#> - /// - Parameters: - /// - to: <#to description#> - /// - t: <#t description#> - /// - control1: <#control1 description#> - /// - control2: <#control2 description#> - /// - Returns: <#description#> func point(to: CGPoint, t: CGFloat, control1: CGPoint, control2: CGPoint) -> CGPoint { let x = CGPoint.value(x: self.x, y: to.x, t: t, control1: control1.x, control2: control2.x) let y = CGPoint.value(x: self.y, y: to.y, t: t, control1: control1.y, control2: control2.x) @@ -393,13 +372,6 @@ extension CGPoint { return CGPoint(x: x, y: y) } - /// <#Description#> - /// - Parameters: - /// - x: <#x description#> - /// - y: <#y description#> - /// - t: <#t description#> - /// - c: <#c description#> - /// - Returns: <#description#> static func value(x: CGFloat, y: CGFloat, t: CGFloat, c: CGFloat) -> CGFloat { var value: CGFloat = 0.0 // (1-t)^2 * p0 + 2 * (1-t) * t * c1 + t^2 * p1 @@ -409,14 +381,6 @@ extension CGPoint { return value } - /// <#Description#> - /// - Parameters: - /// - x: <#x description#> - /// - y: <#y description#> - /// - t: <#t description#> - /// - control1: <#control1 description#> - /// - control2: <#control2 description#> - /// - Returns: <#description#> static func value(x: CGFloat, y: CGFloat, t: CGFloat, control1: CGFloat, control2: CGFloat) -> CGFloat { var value: CGFloat = 0.0 // (1-t)^3 * p0 + 3 * (1-t)^2 * t * c1 + 3 * (1-t) * t^2 * c2 + t^3 * p1 @@ -427,11 +391,6 @@ extension CGPoint { return value } - /// <#Description#> - /// - Parameters: - /// - point1: <#point1 description#> - /// - point2: <#point2 description#> - /// - Returns: <#description#> static func getMidPoint(point1: CGPoint, point2: CGPoint) -> CGPoint { return CGPoint( x: point1.x + (point2.x - point1.x) / 2, @@ -439,29 +398,16 @@ extension CGPoint { ) } - /// <#Description#> - /// - Parameter to: <#to description#> - /// - Returns: <#description#> func dist(to: CGPoint) -> CGFloat { return sqrt((pow(self.x - to.x, 2) + pow(self.y - to.y, 2))) } - /// <#Description#> - /// - Parameters: - /// - firstPoint: <#firstPoint description#> - /// - secondPoint: <#secondPoint description#> - /// - Returns: <#description#> static func midPointForPoints(firstPoint: CGPoint, secondPoint: CGPoint) -> CGPoint { return CGPoint( x: (firstPoint.x + secondPoint.x) / 2, y: (firstPoint.y + secondPoint.y) / 2) } - /// <#Description#> - /// - Parameters: - /// - firstPoint: <#firstPoint description#> - /// - secondPoint: <#secondPoint description#> - /// - Returns: <#description#> static func controlPointForPoints(firstPoint: CGPoint, secondPoint: CGPoint) -> CGPoint { var controlPoint = CGPoint.midPointForPoints(firstPoint: firstPoint, secondPoint: secondPoint) let diffY = abs(secondPoint.y - controlPoint.y) diff --git a/Sources/SwiftUICharts/Base/Extensions/Range+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/Range+Extension.swift new file mode 100644 index 00000000..547a0435 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Extensions/Range+Extension.swift @@ -0,0 +1,7 @@ +import Foundation + +public extension ClosedRange where Bound: AdditiveArithmetic { + var overreach: Bound { + self.upperBound - self.lowerBound + } +} diff --git a/Sources/SwiftUICharts/Base/Extensions/Shape+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/Shape+Extension.swift new file mode 100644 index 00000000..5624e0db --- /dev/null +++ b/Sources/SwiftUICharts/Base/Extensions/Shape+Extension.swift @@ -0,0 +1,17 @@ +import SwiftUI + +extension Shape { + func fill(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: Double = 1) -> some View { + self + .stroke(strokeStyle, lineWidth: lineWidth) + .background(self.fill(fillStyle)) + } +} + +extension InsettableShape { + func fill(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: Double = 1) -> some View { + self + .strokeBorder(strokeStyle, lineWidth: lineWidth) + .background(self.fill(fillStyle)) + } +} diff --git a/Sources/SwiftUICharts/Base/Extensions/View+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/View+Extension.swift index 2ddcea31..1edeb22e 100644 --- a/Sources/SwiftUICharts/Base/Extensions/View+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/View+Extension.swift @@ -8,4 +8,10 @@ extension View { public func chartStyle(_ style: ChartStyle) -> some View { self.environmentObject(style) } + + public func toStandardCoordinateSystem() -> some View { + self + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + } } diff --git a/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift b/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift index eb48a98e..ba8bfe8d 100644 --- a/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift +++ b/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift @@ -1,53 +1,24 @@ import SwiftUI -public struct ChartGrid: View, ChartBase { - public var chartData = ChartData() +public struct ChartGrid: View { let content: () -> Content - let numberOfHorizontalLines = 4 - - @EnvironmentObject var data: ChartData - @EnvironmentObject var style: ChartStyle + public var gridOptions = GridOptions() public init(@ViewBuilder content: @escaping () -> Content) { self.content = content } public var body: some View { - HStack { - ZStack { - VStack { - ForEach(0.. Path { - let baseLine: CGFloat = CGFloat(frame.height / 2) - var hLine = Path() - hLine.move(to: CGPoint(x:0, y: baseLine)) - hLine.addLine(to: CGPoint(x: frame.width, y: baseLine)) - return hLine - } - - var body: some View { - GeometryReader { geometry in - line(frame: geometry.frame(in: .local)) - .stroke(Color(white: 0.85), style: StrokeStyle(lineWidth: 1, lineCap: .round, dash: [5, 10])) - } - } -} - diff --git a/Sources/SwiftUICharts/Base/Grid/ChartGridBaseShape.swift b/Sources/SwiftUICharts/Base/Grid/ChartGridBaseShape.swift new file mode 100644 index 00000000..6b9bd68c --- /dev/null +++ b/Sources/SwiftUICharts/Base/Grid/ChartGridBaseShape.swift @@ -0,0 +1,18 @@ +import SwiftUI + +struct ChartGridBaseShape: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + path.move(to: CGPoint(x: 0, y: 0)) + path.addLine(to: CGPoint(x: rect.width, y: 0)) + return path + } +} + +struct ChartGridBaseShape_Previews: PreviewProvider { + static var previews: some View { + ChartGridBaseShape() + .stroke() + .rotationEffect(.degrees(180), anchor: .center) + } +} diff --git a/Sources/SwiftUICharts/Base/Grid/ChartGridShape.swift b/Sources/SwiftUICharts/Base/Grid/ChartGridShape.swift new file mode 100644 index 00000000..ccb6e18b --- /dev/null +++ b/Sources/SwiftUICharts/Base/Grid/ChartGridShape.swift @@ -0,0 +1,28 @@ +import SwiftUI + +struct ChartGridShape: Shape { + var numberOfHorizontalLines: Int + var numberOfVerticalLines: Int + + func path(in rect: CGRect) -> Path { + let path = Path.drawGridLines(numberOfHorizontalLines: numberOfHorizontalLines, + numberOfVerticalLines: numberOfVerticalLines, + in: rect) + return path + } +} + +struct ChartGridShape_Previews: PreviewProvider { + static var previews: some View { + Group { + ChartGridShape(numberOfHorizontalLines: 5, numberOfVerticalLines: 0) + .stroke() + .toStandardCoordinateSystem() + + ChartGridShape(numberOfHorizontalLines: 4, numberOfVerticalLines: 4) + .stroke() + .toStandardCoordinateSystem() + } + .padding() + } +} diff --git a/Sources/SwiftUICharts/Base/Grid/Extension/Grid+Extension.swift b/Sources/SwiftUICharts/Base/Grid/Extension/Grid+Extension.swift new file mode 100644 index 00000000..05e15ed8 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Grid/Extension/Grid+Extension.swift @@ -0,0 +1,31 @@ +import SwiftUI + +extension ChartGrid { + public func setNumberOfHorizontalLines(_ numberOfLines: Int) -> ChartGrid { + self.gridOptions.numberOfHorizontalLines = numberOfLines + return self + } + + public func setNumberOfVerticalLines(_ numberOfLines: Int) -> ChartGrid { + self.gridOptions.numberOfVerticalLines = numberOfLines + return self + } + + public func setStoreStyle(_ strokeStyle: StrokeStyle) -> ChartGrid { + self.gridOptions.strokeStyle = strokeStyle + return self + } + + public func setColor(_ color: Color) -> ChartGrid { + self.gridOptions.color = color + return self + } + + public func showBaseLine(_ show: Bool, with style: StrokeStyle? = nil) -> ChartGrid { + self.gridOptions.showBaseLine = show + if let style = style { + self.gridOptions.baseStrokeStyle = style + } + return self + } +} diff --git a/Sources/SwiftUICharts/Base/Grid/Model/GridOptions.swift b/Sources/SwiftUICharts/Base/Grid/Model/GridOptions.swift new file mode 100644 index 00000000..2e107a30 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Grid/Model/GridOptions.swift @@ -0,0 +1,14 @@ +import SwiftUI + +public final class GridOptions: ObservableObject { + @Published public var numberOfHorizontalLines: Int = 3 + @Published public var numberOfVerticalLines: Int = 3 + @Published public var strokeStyle: StrokeStyle = StrokeStyle(lineWidth: 1, lineCap: .round, dash: [5, 10]) + @Published public var color: Color = Color(white: 0.85) + @Published public var showBaseLine: Bool = true + @Published public var baseStrokeStyle: StrokeStyle = StrokeStyle(lineWidth: 1.5, lineCap: .round, dash: [5, 0]) + + public init() { + // no-op + } +} diff --git a/Sources/SwiftUICharts/Base/Style/ChartStyle.swift b/Sources/SwiftUICharts/Base/Style/ChartStyle.swift index dafdaece..5ecc3a44 100644 --- a/Sources/SwiftUICharts/Base/Style/ChartStyle.swift +++ b/Sources/SwiftUICharts/Base/Style/ChartStyle.swift @@ -1,44 +1,24 @@ import SwiftUI -/// Descripton of colors/styles for any kind of chart public class ChartStyle: ObservableObject { - - /// colors for background are of chart public let backgroundColor: ColorGradient - /// colors for foreground fill of chart public let foregroundColor: [ColorGradient] - /// Initialize with a single background color and an array of `ColorGradient` for the foreground - /// - Parameters: - /// - backgroundColor: a `Color` - /// - foregroundColor: array of `ColorGradient` public init(backgroundColor: Color, foregroundColor: [ColorGradient]) { self.backgroundColor = ColorGradient.init(backgroundColor) self.foregroundColor = foregroundColor } - /// Initialize with a single background color and a single `ColorGradient` for the foreground - /// - Parameters: - /// - backgroundColor: a `Color` - /// - foregroundColor: a `ColorGradient` public init(backgroundColor: Color, foregroundColor: ColorGradient) { self.backgroundColor = ColorGradient.init(backgroundColor) self.foregroundColor = [foregroundColor] } - /// Initialize with a single background `ColorGradient` and a single `ColorGradient` for the foreground - /// - Parameters: - /// - backgroundColor: a `ColorGradient` - /// - foregroundColor: a `ColorGradient` public init(backgroundColor: ColorGradient, foregroundColor: ColorGradient) { self.backgroundColor = backgroundColor self.foregroundColor = [foregroundColor] } - /// Initialize with a single background `ColorGradient` and an array of `ColorGradient` for the foreground - /// - Parameters: - /// - backgroundColor: a `ColorGradient` - /// - foregroundColor: array of `ColorGradient` public init(backgroundColor: ColorGradient, foregroundColor: [ColorGradient]) { self.backgroundColor = backgroundColor self.foregroundColor = foregroundColor diff --git a/Sources/SwiftUICharts/Base/Style/ColorGradient.swift b/Sources/SwiftUICharts/Base/Style/ColorGradient.swift index 0258d3c1..6625428e 100644 --- a/Sources/SwiftUICharts/Base/Style/ColorGradient.swift +++ b/Sources/SwiftUICharts/Base/Style/ColorGradient.swift @@ -1,39 +1,25 @@ import SwiftUI -/// An encapsulation of a simple gradient between one color and another public struct ColorGradient: Equatable { public let startColor: Color public let endColor: Color - /// Initialize as a solid color - /// - Parameter color: a single `Color` (no gradient effect visible) public init(_ color: Color) { self.startColor = color self.endColor = color } - /// Initialize a color gradient from two specified colors - /// - Parameters: - /// - startColor: starting color - /// - endColor: ending color public init(_ startColor: Color, _ endColor: Color) { self.startColor = startColor self.endColor = endColor } - /// Convert to a `Gradient` object (more complicated than just two colors) - /// - Returns: a `Gradient` between the specified start and end colors public var gradient: Gradient { return Gradient(colors: [startColor, endColor]) } } extension ColorGradient { - /// Convenience method to return a SwiftUI LinearGradient view from the ColorGradient - /// - Parameters: - /// - startPoint: starting point - /// - endPoint: ending point - /// - Returns: a Linear gradient public func linearGradient(from startPoint: UnitPoint, to endPoint: UnitPoint) -> LinearGradient { return LinearGradient(gradient: self.gradient, startPoint: startPoint, endPoint: endPoint) } diff --git a/Sources/SwiftUICharts/Base/Style/Colors.swift b/Sources/SwiftUICharts/Base/Style/Colors.swift index 1590fbcf..a230e901 100644 --- a/Sources/SwiftUICharts/Base/Style/Colors.swift +++ b/Sources/SwiftUICharts/Base/Style/Colors.swift @@ -1,8 +1,6 @@ import SwiftUI -/// Some predefined colors, used for demos, defaults if color is missing, and data indicator point public enum ChartColors { - // Orange public static let orangeBright = Color(hexString: "#FF782C") public static let orangeDark = Color(hexString: "#EC2301") diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift index 6ce279f7..5727831c 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift @@ -1,17 +1,12 @@ import SwiftUI -/// A type of chart that displays vertical bars for each data point -public struct BarChart: View, ChartBase { +public struct BarChart: ChartBase { public var chartData = ChartData() - @EnvironmentObject var data: ChartData @EnvironmentObject var style: ChartStyle - /// The content and behavior of the `BarChart`. - /// - /// public var body: some View { - BarChartRow(chartData: data, style: style) + BarChartRow(chartData: chartData, style: style) } public init() {} diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift index e457d3cd..13153557 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift @@ -1,6 +1,5 @@ import SwiftUI -/// A single vertical bar in a `BarChart` public struct BarChartCell: View { var value: Double var index: Int = 0 @@ -19,9 +18,6 @@ public struct BarChartCell: View { self.touchLocation = touchLocation } - /// The content and behavior of the `BarChartCell`. - /// - /// Animated when first displayed, using the `firstDisplay` variable, with an increasing delay through the data set. public var body: some View { BarChartCellShape(value: didCellAppear ? value : 0.0) .fill(gradientColor.linearGradient(from: .bottom, to: .top)) .onAppear { diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift index 5cfd64b9..1115d30b 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift @@ -1,6 +1,5 @@ import SwiftUI -/// A single row of data, a view in a `BarChart` public struct BarChartRow: View { @EnvironmentObject var chartValue: ChartValue @ObservedObject var chartData: ChartData @@ -15,11 +14,6 @@ public struct BarChartRow: View { return max != 0 ? max : 1 } - /// The content and behavior of the `BarChartRow`. - /// - /// Shows each `BarChartCell` in an `HStack`; may be scaled up if it's the one currently being touched. - /// Not using a drawing group for optimizing animation. - /// As touched (dragged) the `touchLocation` is updated and the current value is highlighted. public var body: some View { GeometryReader { geometry in HStack(alignment: .bottom, @@ -52,11 +46,6 @@ public struct BarChartRow: View { } } - /// Size to scale the touch indicator - /// - Parameters: - /// - touchLocation: fraction of width where touch is happening - /// - index: index into data array - /// - Returns: a scale larger than 1.0 if in bounds; 1.0 (unscaled) if not in bounds func getScaleSize(touchLocation: CGFloat, index: Int) -> CGSize { if touchLocation > CGFloat(index)/CGFloat(chartData.data.count) && touchLocation < CGFloat(index+1)/CGFloat(chartData.data.count) { @@ -65,9 +54,6 @@ public struct BarChartRow: View { return CGSize(width: 1, height: 1) } - /// Get data value where touch happened - /// - Parameter width: width of chart - /// - Returns: value as `Double` if chart has data func getCurrentValue(width: CGFloat) -> Double? { guard self.chartData.data.count > 0 else { return nil} let index = max(0,min(self.chartData.data.count-1,Int(floor((self.touchLocation*width)/(width/CGFloat(self.chartData.data.count)))))) diff --git a/Sources/SwiftUICharts/Charts/LineChart/Extension/LineChart+Extension.swift b/Sources/SwiftUICharts/Charts/LineChart/Extension/LineChart+Extension.swift new file mode 100644 index 00000000..1b2c30b4 --- /dev/null +++ b/Sources/SwiftUICharts/Charts/LineChart/Extension/LineChart+Extension.swift @@ -0,0 +1,24 @@ +import SwiftUI + +extension LineChart { + public func setLineWidth(width: CGFloat) -> LineChart { + self.chartProperties.lineWidth = width + return self + } + + public func setBackground(colorGradient: ColorGradient) -> LineChart { + self.chartProperties.backgroundGradient = colorGradient + return self + } + + public func showChartMarks(_ show: Bool, with color: ColorGradient? = nil) -> LineChart { + self.chartProperties.showChartMarks = show + self.chartProperties.customChartMarksColors = color + return self + } + + public func setLineStyle(to style: LineStyle) -> LineChart { + self.chartProperties.lineStyle = style + return self + } +} diff --git a/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift b/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift index c0249477..69d8d88c 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift @@ -1,10 +1,6 @@ import SwiftUI -/// A dot representing a single data point as user moves finger over line in `LineChart` struct IndicatorPoint: View { - /// The content and behavior of the `IndicatorPoint`. - /// - /// A filled circle with a thick white outline and a shadow public var body: some View { ZStack { Circle() diff --git a/Sources/SwiftUICharts/Charts/LineChart/Line.swift b/Sources/SwiftUICharts/Charts/LineChart/Line.swift index 107367fa..ca2b7cfc 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/Line.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/Line.swift @@ -2,45 +2,48 @@ import SwiftUI /// A single line of data, a view in a `LineChart` public struct Line: View { - @EnvironmentObject var chartValue: ChartValue @ObservedObject var chartData: ChartData + @ObservedObject var chartProperties: LineChartProperties + var curvedLines: Bool = true var style: ChartStyle @State private var showIndicator: Bool = false @State private var touchLocation: CGPoint = .zero - @State private var showBackground: Bool = true @State private var didCellAppear: Bool = false - var curvedLines: Bool = true var path: Path { Path.quadCurvedPathWithPoints(points: chartData.normalisedPoints, step: CGPoint(x: 1.0, y: 1.0)) } + + public init(chartData: ChartData, + style: ChartStyle, + chartProperties: LineChartProperties) { + self.chartData = chartData + self.style = style + self.chartProperties = chartProperties + } - /// The content and behavior of the `Line`. - /// Draw the background if showing the full line (?) and the `showBackground` option is set. Above that draw the line, and then the data indicator if the graph is currently being touched. - /// On appear, set the frame so that the data graph metrics can be calculated. On a drag (touch) gesture, highlight the closest touched data point. - /// TODO: explain rotation public var body: some View { GeometryReader { geometry in ZStack { - if self.didCellAppear && self.showBackground { + if self.didCellAppear, let backgroundColor = chartProperties.backgroundGradient { LineBackgroundShapeView(chartData: chartData, geometry: geometry, - style: style) + backgroundColor: backgroundColor) } LineShapeView(chartData: chartData, + chartProperties: chartProperties, geometry: geometry, style: style, trimTo: didCellAppear ? 1.0 : 0.0) - .animation(.easeIn) + .animation(Animation.easeIn(duration: 0.75)) if self.showIndicator { IndicatorPoint() .position(self.getClosestPointOnPath(geometry: geometry, touchLocation: self.touchLocation)) - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + .toStandardCoordinateSystem() } } .onAppear { @@ -49,20 +52,17 @@ public struct Line: View { .onDisappear() { didCellAppear = false } - - .gesture(DragGesture() - .onChanged({ value in - self.touchLocation = value.location - self.showIndicator = true - self.getClosestDataPoint(geometry: geometry, touchLocation: value.location) - self.chartValue.interactionInProgress = true - }) - .onEnded({ value in - self.touchLocation = .zero - self.showIndicator = false - self.chartValue.interactionInProgress = false - }) - ) +// .gesture(DragGesture() +// .onChanged({ value in +// self.touchLocation = value.location +// self.showIndicator = true +// self.getClosestDataPoint(geometry: geometry, touchLocation: value.location) +// }) +// .onEnded({ value in +// self.touchLocation = .zero +// self.showIndicator = false +// }) +// ) } } } @@ -79,7 +79,7 @@ extension Line { let closest = self.path.point(to: normalisedTouchLocationX) var denormClosest = closest.denormalize(with: geometry) denormClosest.x = denormClosest.x / CGFloat(chartData.normalisedPoints.count - 1) - denormClosest.y = denormClosest.y / CGFloat(chartData.normalisedRange) + denormClosest.y = denormClosest.y / CGFloat(chartData.normalisedYRange) return denormClosest } @@ -89,7 +89,7 @@ extension Line { let geometryWidth = geometry.frame(in: .local).width let index = Int(round((touchLocation.x / geometryWidth) * CGFloat(chartData.points.count - 1))) if (index >= 0 && index < self.chartData.data.count){ - self.chartValue.currentValue = self.chartData.points[index] +// self.chartValue.currentValue = self.chartData.points[index] } } } @@ -103,8 +103,12 @@ struct Line_Previews: PreviewProvider { static var previews: some View { Group { - Line(chartData: ChartData([8, 23, 32, 7, 23, -4]), style: blackLineStyle) - Line(chartData: ChartData([8, 23, 32, 7, 23, 43]), style: redLineStyle) + Line(chartData: ChartData([8, 23, 32, 7, 23, -4]), + style: blackLineStyle, + chartProperties: LineChartProperties()) + Line(chartData: ChartData([8, 23, 32, 7, 23, 43]), + style: redLineStyle, + chartProperties: LineChartProperties()) } } } diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShape.swift b/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShape.swift index 06b02811..3b57733e 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShape.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShape.swift @@ -1,9 +1,9 @@ import SwiftUI struct LineBackgroundShape: Shape { - var data: [Double] + var data: [(Double, Double)] func path(in rect: CGRect) -> Path { - let path = Path.quadClosedCurvedPathWithPoints(points: data, step: CGPoint(x: 1.0, y: 1.0)) + let path = Path.quadClosedCurvedPathWithPoints(data: data, in: rect) return path } } @@ -12,18 +12,14 @@ struct LineBackgroundShape_Previews: PreviewProvider { static var previews: some View { Group { GeometryReader { geometry in - LineBackgroundShape(data: [0, 0.5, 0.8, 0.6, 1]) - .transform(CGAffineTransform(scaleX: geometry.size.width / 4.0, y: geometry.size.height)) + LineBackgroundShape(data: [(0, -0.5), (0.25, 0.8), (0.5,-0.6), (0.75,0.6), (1, 1)]) .fill(Color.red) - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + .toStandardCoordinateSystem() } GeometryReader { geometry in - LineBackgroundShape(data: [0, -0.5, 0.8, -0.6, 1]) - .transform(CGAffineTransform(scaleX: geometry.size.width / 4.0, y: geometry.size.height / 1.6)) + LineBackgroundShape(data: [(0, 0), (0.25, 0.5), (0.5,0.8), (0.75, 0.6), (1, 1)]) .fill(Color.blue) - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + .toStandardCoordinateSystem() } } } diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShapeView.swift b/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShapeView.swift index 25ba8214..2a4f45eb 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShapeView.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShapeView.swift @@ -3,17 +3,14 @@ import SwiftUI struct LineBackgroundShapeView: View { var chartData: ChartData var geometry: GeometryProxy - var style: ChartStyle + var backgroundColor: ColorGradient var body: some View { - LineBackgroundShape(data: chartData.normalisedPoints) - .transform(CGAffineTransform(scaleX: geometry.size.width / CGFloat(chartData.normalisedPoints.count - 1), - y: geometry.size.height / CGFloat(chartData.normalisedRange))) - .fill(LinearGradient(gradient: Gradient(colors: [style.foregroundColor.first?.startColor ?? .white, - style.backgroundColor.startColor]), + LineBackgroundShape(data: chartData.normalisedData) + .fill(LinearGradient(gradient: Gradient(colors: [backgroundColor.startColor, + backgroundColor.endColor]), startPoint: .bottom, endPoint: .top)) - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + .toStandardCoordinateSystem() } } diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift b/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift index 379fbd37..c069ec0b 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift @@ -1,17 +1,14 @@ import SwiftUI -/// A type of chart that displays a line connecting the data points -public struct LineChart: View, ChartBase { +public struct LineChart: ChartBase { public var chartData = ChartData() - - @EnvironmentObject var data: ChartData @EnvironmentObject var style: ChartStyle + public var chartProperties = LineChartProperties() - /// The content and behavior of the `LineChart`. - /// - /// public var body: some View { - Line(chartData: data, style: style) + Line(chartData: chartData, + style: style, + chartProperties: chartProperties) } public init() {} diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineShape.swift b/Sources/SwiftUICharts/Charts/LineChart/LineShape.swift index 64fff658..e22be2f4 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/LineShape.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/LineShape.swift @@ -1,9 +1,16 @@ import SwiftUI struct LineShape: Shape { - var data: [Double] + var data: [(Double, Double)] + var lineStyle: LineStyle = .curved func path(in rect: CGRect) -> Path { - let path = Path.quadCurvedPathWithPoints(points: data, step: CGPoint(x: 1.0, y: 1.0)) + var path = Path() + switch lineStyle { + case .curved: + path = Path.quadCurvedPathWithPoints(data: data, in: rect) + case .straight: + path = Path.linePathWithPoints(data: data, in: rect) + } return path } } @@ -11,20 +18,13 @@ struct LineShape: Shape { struct LineShape_Previews: PreviewProvider { static var previews: some View { Group { - GeometryReader { geometry in - LineShape(data: [0, 0.5, 0.8, 0.6, 1]) - .transform(CGAffineTransform(scaleX: geometry.size.width / 4.0, y: geometry.size.height)) - .stroke(Color.red) - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) - } - GeometryReader { geometry in - LineShape(data: [0, -0.5, 0.8, -0.6, 1]) - .transform(CGAffineTransform(scaleX: geometry.size.width / 4.0, y: geometry.size.height / 1.6)) - .stroke(Color.blue) - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) - } + LineShape(data: [(0, 0), (0.25, 0.5), (0.5,0.8), (0.75, 0.6), (1, 1)]) + .stroke() + .toStandardCoordinateSystem() + + LineShape(data: [(0, -0.5), (0.25, 0.8), (0.5,-0.6), (0.75,0.6), (1, 1)], lineStyle: .straight) + .stroke() + .toStandardCoordinateSystem() } } } diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineShapeView.swift b/Sources/SwiftUICharts/Charts/LineChart/LineShapeView.swift index d7143932..308debbc 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/LineShapeView.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/LineShapeView.swift @@ -2,6 +2,8 @@ import SwiftUI struct LineShapeView: View, Animatable { var chartData: ChartData + var chartProperties: LineChartProperties + var geometry: GeometryProxy var style: ChartStyle var trimTo: Double = 0 @@ -11,16 +13,71 @@ struct LineShapeView: View, Animatable { set { trimTo = Double(newValue) } } + var chartMarkColor: LinearGradient { + if let customColor = chartProperties.customChartMarksColors { + return customColor.linearGradient(from: .leading, to: .trailing) + } + + return LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient, + startPoint: .leading, + endPoint: .trailing) + } + var body: some View { - LineShape(data: chartData.normalisedPoints) - .trim(from: 0, to: CGFloat(trimTo)) - .transform(CGAffineTransform(scaleX: geometry.size.width / CGFloat(chartData.normalisedPoints.count - 1), - y: geometry.size.height / CGFloat(chartData.normalisedRange))) - .stroke(LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient, - startPoint: .leading, - endPoint: .trailing), - style: StrokeStyle(lineWidth: 3, lineJoin: .round)) - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + ZStack { + LineShape(data: chartData.normalisedData, lineStyle: chartProperties.lineStyle) + .trim(from: 0, to: CGFloat(trimTo)) + .stroke(LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient, + startPoint: .leading, + endPoint: .trailing), + style: StrokeStyle(lineWidth: chartProperties.lineWidth, lineJoin: .round)) + .toStandardCoordinateSystem() + .clipped() + if chartProperties.showChartMarks { + MarkerShape(data: chartData.normalisedData) + .trim(from: 0, to: CGFloat(trimTo)) + .fill(.white, + strokeBorder: chartMarkColor, + lineWidth: chartProperties.lineWidth) + .toStandardCoordinateSystem() + } + } } } + +struct LineShapeView_Previews: PreviewProvider { + static let chartData = ChartData([6, 8, 6], rangeY: 6...10) + static let chartDataOutOfRange = ChartData([-1, 8, 6, 12, 3], rangeY: -5...15) + + static let chartDataOutOfRange2 = ChartData([6,6,8,5], rangeY: 5...10) + + static let chartStyle = ChartStyle(backgroundColor: Color.white, + foregroundColor: [ColorGradient(Color.orange, Color.red)]) + static var previews: some View { + Group { + GeometryReader { geometry in + LineShapeView(chartData: chartData, + chartProperties: LineChartProperties(), + geometry: geometry, + style: chartStyle, + trimTo: 1.0) + } + GeometryReader { geometry in + LineShapeView(chartData: chartDataOutOfRange, + chartProperties: LineChartProperties(), + geometry: geometry, + style: chartStyle, + trimTo: 1.0) + } + GeometryReader { geometry in + LineShapeView(chartData: chartDataOutOfRange2, + chartProperties: LineChartProperties(), + geometry: geometry, + style: chartStyle, + trimTo: 1.0) + } + } + } +} + + diff --git a/Sources/SwiftUICharts/Charts/LineChart/MarkerShape.swift b/Sources/SwiftUICharts/Charts/LineChart/MarkerShape.swift new file mode 100644 index 00000000..0207b896 --- /dev/null +++ b/Sources/SwiftUICharts/Charts/LineChart/MarkerShape.swift @@ -0,0 +1,23 @@ +import SwiftUI + +struct MarkerShape: Shape { + var data: [(Double, Double)] + func path(in rect: CGRect) -> Path { + let path = Path.drawChartMarkers(data: data, in: rect) + return path + } +} + +struct MarkerShape_Previews: PreviewProvider { + static var previews: some View { + Group { + MarkerShape(data: [(0, 0), (0.25, 0.5), (0.5,0.8), (0.75, 0.6), (1, 1)]) + .stroke() + .toStandardCoordinateSystem() + + MarkerShape(data: [(0, -0.5), (0.25, 0.8), (0.5,-0.6), (0.75,0.6), (1, 1)]) + .stroke() + .toStandardCoordinateSystem() + } + } +} diff --git a/Sources/SwiftUICharts/Charts/LineChart/Model/LineChartProperties.swift b/Sources/SwiftUICharts/Charts/LineChart/Model/LineChartProperties.swift new file mode 100644 index 00000000..59cc9060 --- /dev/null +++ b/Sources/SwiftUICharts/Charts/LineChart/Model/LineChartProperties.swift @@ -0,0 +1,13 @@ +import SwiftUI + +public class LineChartProperties: ObservableObject { + @Published var lineWidth: CGFloat = 2.0 + @Published var backgroundGradient: ColorGradient? + @Published var showChartMarks: Bool = true + @Published var customChartMarksColors: ColorGradient? + @Published var lineStyle: LineStyle = .curved + + public init() { + // no-op + } +} diff --git a/Sources/SwiftUICharts/Charts/LineChart/Model/LineStyle.swift b/Sources/SwiftUICharts/Charts/LineChart/Model/LineStyle.swift new file mode 100644 index 00000000..c3c3d4a2 --- /dev/null +++ b/Sources/SwiftUICharts/Charts/LineChart/Model/LineStyle.swift @@ -0,0 +1,6 @@ +import Foundation + +public enum LineStyle { + case curved + case straight +} diff --git a/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift b/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift index f23d3c96..63e91260 100644 --- a/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift +++ b/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift @@ -1,17 +1,16 @@ import SwiftUI /// A type of chart that displays a slice of "pie" for each data point -public struct PieChart: View, ChartBase { +public struct PieChart: ChartBase { public var chartData = ChartData() - @EnvironmentObject var data: ChartData @EnvironmentObject var style: ChartStyle /// The content and behavior of the `PieChart`. /// /// public var body: some View { - PieChartRow(chartData: data, style: style) + PieChartRow(chartData: chartData, style: style) } public init() {} diff --git a/Sources/SwiftUICharts/Charts/RingsChart/RingsChart.swift b/Sources/SwiftUICharts/Charts/RingsChart/RingsChart.swift index 1f59a651..a46e0408 100644 --- a/Sources/SwiftUICharts/Charts/RingsChart/RingsChart.swift +++ b/Sources/SwiftUICharts/Charts/RingsChart/RingsChart.swift @@ -1,22 +1,14 @@ -// -// RingsChart.swift -// ChartViewV2Demo -// -// Created by Dan Wood on 8/20/20. -// - import SwiftUI -public struct RingsChart: View, ChartBase { +public struct RingsChart: ChartBase { public var chartData = ChartData() - @EnvironmentObject var data: ChartData @EnvironmentObject var style: ChartStyle // TODO - should put background opacity, ring width & spacing as chart style values public var body: some View { - RingsChartRow(width:10.0, spacing:5.0, chartData: data, style: style) + RingsChartRow(width:10.0, spacing:5.0, chartData: chartData, style: style) } public init() {} From 7fd5b185f867f0bb323e53ad6ce9d9a8bbba0539 Mon Sep 17 00:00:00 2001 From: Andras Samu Date: Sat, 26 Nov 2022 14:34:45 +0100 Subject: [PATCH 28/29] Feat/new protocol and range (#255) * feat: new protocol for chained functions, and added support for explicit Y ranges. X coming as well * feat: add new axis interface (#253) From 7140b8b6fd7413ccbcf31b748b32233f08314dab Mon Sep 17 00:00:00 2001 From: Andras Samu Date: Sat, 26 Nov 2022 14:37:17 +0100 Subject: [PATCH 29/29] feat: add animation toggle interface (#256) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: “Andras <“andras.samu@supercharge.io”> --- .../Extension/LineChart+Extension.swift | 5 +++ .../SwiftUICharts/Charts/LineChart/Line.swift | 42 +++++++++---------- .../LineChart/Model/LineChartProperties.swift | 2 +- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/Sources/SwiftUICharts/Charts/LineChart/Extension/LineChart+Extension.swift b/Sources/SwiftUICharts/Charts/LineChart/Extension/LineChart+Extension.swift index 1b2c30b4..03920e1e 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/Extension/LineChart+Extension.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/Extension/LineChart+Extension.swift @@ -21,4 +21,9 @@ extension LineChart { self.chartProperties.lineStyle = style return self } + + public func withAnimation(_ enabled: Bool) -> LineChart { + self.chartProperties.animationEnabled = enabled + return self + } } diff --git a/Sources/SwiftUICharts/Charts/LineChart/Line.swift b/Sources/SwiftUICharts/Charts/LineChart/Line.swift index ca2b7cfc..2e2fc214 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/Line.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/Line.swift @@ -33,18 +33,7 @@ public struct Line: View { geometry: geometry, backgroundColor: backgroundColor) } - LineShapeView(chartData: chartData, - chartProperties: chartProperties, - geometry: geometry, - style: style, - trimTo: didCellAppear ? 1.0 : 0.0) - .animation(Animation.easeIn(duration: 0.75)) - if self.showIndicator { - IndicatorPoint() - .position(self.getClosestPointOnPath(geometry: geometry, - touchLocation: self.touchLocation)) - .toStandardCoordinateSystem() - } + lineShapeView(geometry: geometry) } .onAppear { didCellAppear = true @@ -52,17 +41,24 @@ public struct Line: View { .onDisappear() { didCellAppear = false } -// .gesture(DragGesture() -// .onChanged({ value in -// self.touchLocation = value.location -// self.showIndicator = true -// self.getClosestDataPoint(geometry: geometry, touchLocation: value.location) -// }) -// .onEnded({ value in -// self.touchLocation = .zero -// self.showIndicator = false -// }) -// ) + } + } + + @ViewBuilder + private func lineShapeView(geometry: GeometryProxy) -> some View { + if chartProperties.animationEnabled { + LineShapeView(chartData: chartData, + chartProperties: chartProperties, + geometry: geometry, + style: style, + trimTo: didCellAppear ? 1.0 : 0.0) + .animation(Animation.easeIn(duration: 0.75)) + } else { + LineShapeView(chartData: chartData, + chartProperties: chartProperties, + geometry: geometry, + style: style, + trimTo: 1.0) } } } diff --git a/Sources/SwiftUICharts/Charts/LineChart/Model/LineChartProperties.swift b/Sources/SwiftUICharts/Charts/LineChart/Model/LineChartProperties.swift index 59cc9060..24893be2 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/Model/LineChartProperties.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/Model/LineChartProperties.swift @@ -6,7 +6,7 @@ public class LineChartProperties: ObservableObject { @Published var showChartMarks: Bool = true @Published var customChartMarksColors: ColorGradient? @Published var lineStyle: LineStyle = .curved - + @Published var animationEnabled: Bool = true public init() { // no-op }