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/.swiftpm/xcode/package.xcworkspace/xcuserdata/samuandris.xcuserdatad/UserInterfaceState.xcuserstate b/.swiftpm/xcode/package.xcworkspace/xcuserdata/samuandris.xcuserdatad/UserInterfaceState.xcuserstate
new file mode 100644
index 00000000..a5f64f0a
Binary files /dev/null and b/.swiftpm/xcode/package.xcworkspace/xcuserdata/samuandris.xcuserdatad/UserInterfaceState.xcuserstate differ
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/.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 540c36e2..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
- 1
+ 0
+
+
+ SuppressBuildableAutocreation
+
+ SwiftUICharts
+
+ primary
+
+
+ SwiftUIChartsTests
+
+ primary
+
diff --git a/Package.swift b/Package.swift
index ffd10e06..3339018c 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), .macOS(.v10_15)
],
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/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/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/CardView/CardView.swift b/Sources/SwiftUICharts/Base/CardView/CardView.swift
new file mode 100644
index 00000000..e5d7eb3a
--- /dev/null
+++ b/Sources/SwiftUICharts/Base/CardView/CardView.swift
@@ -0,0 +1,37 @@
+import SwiftUI
+
+/// View containing data and some kind of chart content
+public struct CardView: View, ChartBase {
+ public var chartData = ChartData()
+ let content: () -> Content
+
+ private var showShadow: Bool
+
+ @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 {
+ RoundedRectangle(cornerRadius: 20)
+ .fill(Color.white)
+ .shadow(color: Color(white: 0.9, opacity: 1), radius: 8)
+ }
+ VStack (alignment: .leading) {
+ self.content()
+ }
+ .clipShape(RoundedRectangle(cornerRadius: showShadow ? 20 : 0))
+ }
+ }
+}
diff --git a/Sources/SwiftUICharts/Base/Chart/ChartBase.swift b/Sources/SwiftUICharts/Base/Chart/ChartBase.swift
new file mode 100644
index 00000000..f1876dcb
--- /dev/null
+++ b/Sources/SwiftUICharts/Base/Chart/ChartBase.swift
@@ -0,0 +1,6 @@
+import SwiftUI
+
+/// Protocol for any type of chart, to get access to underlying data
+public protocol ChartBase: View {
+ var chartData: ChartData { get }
+}
diff --git a/Sources/SwiftUICharts/Base/Chart/ChartData.swift b/Sources/SwiftUICharts/Base/Chart/ChartData.swift
new file mode 100644
index 00000000..f1eeec36
--- /dev/null
+++ b/Sources/SwiftUICharts/Base/Chart/ChartData.swift
@@ -0,0 +1,74 @@
+import SwiftUI
+
+/// An observable wrapper for an array of data for use in any chart
+public class ChartData: ObservableObject {
+ @Published public var data: [(Double, Double)] = []
+ public var rangeY: ClosedRange?
+ public var rangeX: ClosedRange?
+
+ var points: [Double] {
+ data.filter { rangeX?.contains($0.0) ?? true }.map { $0.1 }
+ }
+
+ var values: [Double] {
+ data.filter { rangeX?.contains($0.0) ?? true }.map { $0.0 }
+ }
+
+ var normalisedPoints: [Double] {
+ let absolutePoints = points.map { abs($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 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 {
+ 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], rangeY: ClosedRange? = nil) {
+ self.data = data.enumerated().map{ (index, value) in (Double(index), value) }
+ self.rangeY = rangeY
+ }
+
+ public init(_ data: [(Double, Double)], rangeY: ClosedRange? = nil) {
+ self.data = data
+ self.rangeY = rangeY
+ }
+
+ public init() {
+ self.data = []
+ }
+}
diff --git a/Sources/SwiftUICharts/Base/Chart/ChartValue.swift b/Sources/SwiftUICharts/Base/Chart/ChartValue.swift
new file mode 100644
index 00000000..48ff0c99
--- /dev/null
+++ b/Sources/SwiftUICharts/Base/Chart/ChartValue.swift
@@ -0,0 +1,7 @@
+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/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
new file mode 100644
index 00000000..1e4bedd7
--- /dev/null
+++ b/Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift
@@ -0,0 +1,26 @@
+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
+ }
+
+ if self.count <= index {
+ return self[index % self.count]
+ }
+
+ 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/CGPoint+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift
new file mode 100644
index 00000000..31c401d9
--- /dev/null
+++ b/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift
@@ -0,0 +1,47 @@
+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 = 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)
+ }
+
+ 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/Extensions/CGRect+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/CGRect+Extension.swift
new file mode 100644
index 00000000..e66ee562
--- /dev/null
+++ b/Sources/SwiftUICharts/Base/Extensions/CGRect+Extension.swift
@@ -0,0 +1,11 @@
+import Foundation
+import SwiftUI
+
+extension CGRect {
+
+ /// 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
new file mode 100644
index 00000000..96ec022a
--- /dev/null
+++ b/Sources/SwiftUICharts/Base/Extensions/ChartBase+Extension.swift
@@ -0,0 +1,23 @@
+import SwiftUI
+
+extension ChartBase {
+ public func data(_ data: [Double]) -> some ChartBase {
+ chartData.data = data.enumerated().map{ (index, value) in (Double(index), value) }
+ return self
+ }
+
+ public func data(_ data: [(Double, Double)]) -> some ChartBase {
+ chartData.data = data
+ return self
+ }
+
+ 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/Color+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift
new file mode 100644
index 00000000..6f3bd22f
--- /dev/null
+++ b/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift
@@ -0,0 +1,25 @@
+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()
+ Scanner(string: hex).scanHexInt64(&int)
+ let red, green, blue: UInt64
+ switch hex.count {
+ case 3: // RGB (12-bit)
+ (red, green, blue) = ((int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
+ 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)
+ }
+ self.init(red: Double(red) / 255, green: Double(green) / 255, blue: Double(blue) / 255)
+ }
+}
diff --git a/Sources/SwiftUICharts/LineChart/Path+QuadCurve.swift b/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift
similarity index 54%
rename from Sources/SwiftUICharts/LineChart/Path+QuadCurve.swift
rename to Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift
index 83cf114b..8085fd0c 100644
--- a/Sources/SwiftUICharts/LineChart/Path+QuadCurve.swift
+++ b/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift
@@ -1,37 +1,30 @@
-//
-// 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)
-
+
+ // 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)
}
-
+
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?
@@ -63,7 +56,7 @@ extension Path {
}
return ret
}
-
+
func length(to maxX: CGFloat) -> CGFloat {
var ret: CGFloat = 0.0
var start: CGPoint?
@@ -114,78 +107,153 @@ 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)
+ // 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.. 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 p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
- path.addLine(to: p1)
- 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.. 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 p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
- path.move(to: p1)
- for pointIndex in 1.. Path {
+
+ static func closedLinePathWithPoints(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 }
- var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
- path.move(to: p1)
- for pointIndex in 1.. 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
@@ -220,7 +288,7 @@ extension CGPoint {
}
return dist
}
-
+
func quadCurve(to: CGPoint, control: CGPoint, x: CGFloat) -> CGFloat {
var dist: CGFloat = 0
let steps: CGFloat = 100
@@ -245,14 +313,14 @@ extension CGPoint {
}
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
@@ -269,7 +337,7 @@ extension CGPoint {
return dist
}
-
+
func curve(to: CGPoint, control1: CGPoint, control2: CGPoint, x: CGFloat) -> CGFloat {
var dist: CGFloat = 0
let steps: CGFloat = 100
@@ -296,14 +364,14 @@ extension CGPoint {
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)
+ 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)
}
-
+
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
@@ -312,42 +380,43 @@ extension CGPoint {
value += pow(t, 2) * y
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
}
-
+
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 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/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
new file mode 100644
index 00000000..1edeb22e
--- /dev/null
+++ b/Sources/SwiftUICharts/Base/Extensions/View+Extension.swift
@@ -0,0 +1,17 @@
+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)
+ }
+
+ 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
new file mode 100644
index 00000000..ba8bfe8d
--- /dev/null
+++ b/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift
@@ -0,0 +1,24 @@
+import SwiftUI
+
+public struct ChartGrid: View {
+ let content: () -> Content
+ public var gridOptions = GridOptions()
+
+ public init(@ViewBuilder content: @escaping () -> Content) {
+ self.content = content
+ }
+
+ public var body: some View {
+ ZStack {
+ ChartGridShape(numberOfHorizontalLines: gridOptions.numberOfHorizontalLines,
+ numberOfVerticalLines: gridOptions.numberOfVerticalLines)
+ .stroke(gridOptions.color, style: gridOptions.strokeStyle)
+ if gridOptions.showBaseLine {
+ ChartGridBaseShape()
+ .stroke(gridOptions.color, style: gridOptions.baseStrokeStyle)
+ .rotationEffect(.degrees(180), anchor: .center)
+ }
+ self.content()
+ }
+ }
+}
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/Label/ChartLabel.swift b/Sources/SwiftUICharts/Base/Label/ChartLabel.swift
new file mode 100644
index 00000000..fb1a8909
--- /dev/null
+++ b/Sources/SwiftUICharts/Base/Label/ChartLabel.swift
@@ -0,0 +1,107 @@
+import SwiftUI
+
+/// What kind of label - this affects color, size, position of the label
+public enum ChartLabelType {
+ case title
+ case subTitle
+ case largeTitle
+ case custom(size: CGFloat, padding: EdgeInsets, color: Color)
+ 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 = ""
+ var format: String = "%.01f"
+
+ private var title: String
+
+ /// Label font size
+ /// - Returns: the font size of the label
+ 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
+ }
+ }
+
+ /// Padding around label
+ /// - Returns: the edge padding to use based on position of the label
+ private var labelPadding: EdgeInsets {
+ switch labelType {
+ case .title:
+ return EdgeInsets(top: 16.0, leading: 0, bottom: 0.0, trailing: 8.0)
+ case .legend:
+ return EdgeInsets(top: 4.0, leading: 0, bottom: 0.0, trailing: 8.0)
+ case .subTitle:
+ return EdgeInsets(top: 8.0, leading: 0, bottom: 0.0, trailing: 8.0)
+ case .largeTitle:
+ return EdgeInsets(top: 24.0, leading: 0, bottom: 0.0, trailing: 8.0)
+ case .custom(_, let padding, _):
+ return padding
+ }
+ }
+
+ /// 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:
+ return Color.primary
+ case .legend:
+ return Color.secondary
+ case .subTitle:
+ return Color.primary
+ case .largeTitle:
+ return Color.primary
+ case .custom(_, _, let color):
+ return color
+ }
+ }
+
+ /// Initialize
+ /// - Parameters:
+ /// - title: Any `String`
+ /// - type: Which `ChartLabelType` to use
+ 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`.
+ ///
+ /// 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)
+ .font(.system(size: labelSize))
+ .bold()
+ .foregroundColor(self.labelColor)
+ .padding(self.labelPadding)
+ .onAppear {
+ self.textToDisplay = self.title
+ }
+ .onReceive(self.chartValue.objectWillChange) { _ in
+ self.textToDisplay = self.chartValue.interactionInProgress ? String(format: format, self.chartValue.currentValue) : self.title
+ }
+ if !self.chartValue.interactionInProgress {
+ Spacer()
+ }
+ }
+ }
+}
diff --git a/Sources/SwiftUICharts/Base/Style/ChartStyle.swift b/Sources/SwiftUICharts/Base/Style/ChartStyle.swift
new file mode 100644
index 00000000..5ecc3a44
--- /dev/null
+++ b/Sources/SwiftUICharts/Base/Style/ChartStyle.swift
@@ -0,0 +1,27 @@
+import SwiftUI
+
+public class ChartStyle: ObservableObject {
+ 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
new file mode 100644
index 00000000..6625428e
--- /dev/null
+++ b/Sources/SwiftUICharts/Base/Style/ColorGradient.swift
@@ -0,0 +1,33 @@
+import SwiftUI
+
+public struct ColorGradient: Equatable {
+ 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
+ }
+
+ public var gradient: Gradient {
+ return Gradient(colors: [startColor, endColor])
+ }
+}
+
+extension ColorGradient {
+ 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/Base/Style/Colors.swift b/Sources/SwiftUICharts/Base/Style/Colors.swift
new file mode 100644
index 00000000..a230e901
--- /dev/null
+++ b/Sources/SwiftUICharts/Base/Style/Colors.swift
@@ -0,0 +1,9 @@
+import SwiftUI
+
+public enum ChartColors {
+ public static let orangeBright = Color(hexString: "#FF782C")
+ public static let orangeDark = Color(hexString: "#EC2301")
+
+ public static let legendColor: Color = Color(hexString: "#E8E7EA")
+ public 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..5727831c
--- /dev/null
+++ b/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift
@@ -0,0 +1,13 @@
+import SwiftUI
+
+public struct BarChart: ChartBase {
+ public var chartData = ChartData()
+
+ @EnvironmentObject var style: ChartStyle
+
+ public var body: some View {
+ BarChartRow(chartData: chartData, style: style)
+ }
+
+ public init() {}
+}
diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift
new file mode 100644
index 00000000..13153557
--- /dev/null
+++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift
@@ -0,0 +1,52 @@
+import SwiftUI
+
+public struct BarChartCell: View {
+ var value: Double
+ var index: Int = 0
+ var gradientColor: ColorGradient
+ var touchLocation: CGFloat
+
+ @State private var didCellAppear: Bool = false
+
+ public init( value: Double,
+ index: Int = 0,
+ gradientColor: ColorGradient,
+ touchLocation: CGFloat) {
+ self.value = value
+ self.index = index
+ self.gradientColor = gradientColor
+ self.touchLocation = touchLocation
+ }
+
+ public var body: some View {
+ BarChartCellShape(value: didCellAppear ? value : 0.0)
+ .fill(gradientColor.linearGradient(from: .bottom, to: .top)) .onAppear {
+ self.didCellAppear = true
+ }
+ .onDisappear {
+ self.didCellAppear = false
+ }
+ .transition(.slide)
+ .animation(Animation.spring().delay(self.touchLocation < 0 || !didCellAppear ? Double(self.index) * 0.04 : 0))
+ }
+}
+
+struct BarChartCell_Previews: PreviewProvider {
+ static var previews: some View {
+ Group {
+ Group {
+ BarChartCell(value: 0, gradientColor: ColorGradient.greenRed, 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, 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..268f4229
--- /dev/null
+++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartCellShape.swift
@@ -0,0 +1,49 @@
+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: 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: value < 0 ? Double.pi/2 : -Double.pi/2),
+ endAngle: Angle(radians: 0),
+ clockwise: value < 0 ? true : 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)
+
+ BarChartCellShape(value: -0.3)
+ .fill(Color.blue)
+ .offset(x: 0, y: -600)
+ }
+ }
+}
diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift
new file mode 100644
index 00000000..1115d30b
--- /dev/null
+++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift
@@ -0,0 +1,70 @@
+import SwiftUI
+
+public struct BarChartRow: View {
+ @EnvironmentObject var chartValue: ChartValue
+ @ObservedObject var chartData: ChartData
+ @State private var touchLocation: CGFloat = -1.0
+
+ var style: ChartStyle
+
+ var maxValue: Double {
+ guard let max = chartData.points.max() else {
+ return 1
+ }
+ return max != 0 ? max : 1
+ }
+
+ public var body: some View {
+ GeometryReader { geometry in
+ HStack(alignment: .bottom,
+ spacing: geometry.frame(in: .local).width / CGFloat(chartData.data.count * 3)) {
+ ForEach(0.. CGSize {
+ 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)
+ }
+
+ 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.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/Extension/LineChart+Extension.swift b/Sources/SwiftUICharts/Charts/LineChart/Extension/LineChart+Extension.swift
new file mode 100644
index 00000000..03920e1e
--- /dev/null
+++ b/Sources/SwiftUICharts/Charts/LineChart/Extension/LineChart+Extension.swift
@@ -0,0 +1,29 @@
+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
+ }
+
+ public func withAnimation(_ enabled: Bool) -> LineChart {
+ self.chartProperties.animationEnabled = enabled
+ return self
+ }
+}
diff --git a/Sources/SwiftUICharts/LineChart/IndicatorPoint.swift b/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift
similarity index 52%
rename from Sources/SwiftUICharts/LineChart/IndicatorPoint.swift
rename to Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift
index 2e8667da..69d8d88c 100644
--- a/Sources/SwiftUICharts/LineChart/IndicatorPoint.swift
+++ b/Sources/SwiftUICharts/Charts/LineChart/IndicatorPoint.swift
@@ -1,23 +1,15 @@
-//
-// 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{
+ public var body: some View {
+ ZStack {
Circle()
- .fill(Colors.IndicatorKnob)
+ .fill(ChartColors.indicatorKnob)
Circle()
.stroke(Color.white, style: StrokeStyle(lineWidth: 4))
}
.frame(width: 14, height: 14)
- .shadow(color: Colors.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
new file mode 100644
index 00000000..2e2fc214
--- /dev/null
+++ b/Sources/SwiftUICharts/Charts/LineChart/Line.swift
@@ -0,0 +1,111 @@
+import SwiftUI
+
+/// A single line of data, a view in a `LineChart`
+public struct Line: View {
+ @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 didCellAppear: Bool = false
+
+ 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
+ }
+
+ public var body: some View {
+ GeometryReader { geometry in
+ ZStack {
+ if self.didCellAppear, let backgroundColor = chartProperties.backgroundGradient {
+ LineBackgroundShapeView(chartData: chartData,
+ geometry: geometry,
+ backgroundColor: backgroundColor)
+ }
+ lineShapeView(geometry: geometry)
+ }
+ .onAppear {
+ didCellAppear = true
+ }
+ .onDisappear() {
+ didCellAppear = 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)
+ }
+ }
+}
+
+// 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(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.normalisedYRange)
+ return denormClosest
+ }
+
+// /// Figure out where closest touch point was
+// /// - Parameter point: location of data point on graph, near touch location
+ 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
+ static let blackLineStyle = ChartStyle(backgroundColor: ColorGradient(.white), foregroundColor: ColorGradient(.black))
+
+ /// Predefined style red over white, for preview
+ static let redLineStyle = ChartStyle(backgroundColor: .whiteBlack, foregroundColor: ColorGradient(.red))
+
+ static var previews: some View {
+ Group {
+ 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
new file mode 100644
index 00000000..3b57733e
--- /dev/null
+++ b/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShape.swift
@@ -0,0 +1,27 @@
+import SwiftUI
+
+struct LineBackgroundShape: Shape {
+ var data: [(Double, Double)]
+ func path(in rect: CGRect) -> Path {
+ let path = Path.quadClosedCurvedPathWithPoints(data: data, in: rect)
+ return path
+ }
+}
+
+struct LineBackgroundShape_Previews: PreviewProvider {
+ static var previews: some View {
+ Group {
+ GeometryReader { geometry in
+ LineBackgroundShape(data: [(0, -0.5), (0.25, 0.8), (0.5,-0.6), (0.75,0.6), (1, 1)])
+ .fill(Color.red)
+ .toStandardCoordinateSystem()
+ }
+ GeometryReader { geometry in
+ LineBackgroundShape(data: [(0, 0), (0.25, 0.5), (0.5,0.8), (0.75, 0.6), (1, 1)])
+ .fill(Color.blue)
+ .toStandardCoordinateSystem()
+ }
+ }
+ }
+}
+
diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShapeView.swift b/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShapeView.swift
new file mode 100644
index 00000000..2a4f45eb
--- /dev/null
+++ b/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShapeView.swift
@@ -0,0 +1,16 @@
+import SwiftUI
+
+struct LineBackgroundShapeView: View {
+ var chartData: ChartData
+ var geometry: GeometryProxy
+ var backgroundColor: ColorGradient
+
+ var body: some View {
+ LineBackgroundShape(data: chartData.normalisedData)
+ .fill(LinearGradient(gradient: Gradient(colors: [backgroundColor.startColor,
+ backgroundColor.endColor]),
+ startPoint: .bottom,
+ endPoint: .top))
+ .toStandardCoordinateSystem()
+ }
+}
diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift b/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift
new file mode 100644
index 00000000..c069ec0b
--- /dev/null
+++ b/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift
@@ -0,0 +1,15 @@
+import SwiftUI
+
+public struct LineChart: ChartBase {
+ public var chartData = ChartData()
+ @EnvironmentObject var style: ChartStyle
+ public var chartProperties = LineChartProperties()
+
+ public var body: some View {
+ 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
new file mode 100644
index 00000000..e22be2f4
--- /dev/null
+++ b/Sources/SwiftUICharts/Charts/LineChart/LineShape.swift
@@ -0,0 +1,30 @@
+import SwiftUI
+
+struct LineShape: Shape {
+ var data: [(Double, Double)]
+ var lineStyle: LineStyle = .curved
+ func path(in rect: CGRect) -> Path {
+ 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
+ }
+}
+
+struct LineShape_Previews: PreviewProvider {
+ static var previews: some View {
+ Group {
+ 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
new file mode 100644
index 00000000..308debbc
--- /dev/null
+++ b/Sources/SwiftUICharts/Charts/LineChart/LineShapeView.swift
@@ -0,0 +1,83 @@
+import SwiftUI
+
+struct LineShapeView: View, Animatable {
+ var chartData: ChartData
+ var chartProperties: LineChartProperties
+
+ var geometry: GeometryProxy
+ var style: ChartStyle
+ var trimTo: Double = 0
+
+ var animatableData: CGFloat {
+ get { CGFloat(trimTo) }
+ 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 {
+ 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..24893be2
--- /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
+ @Published var animationEnabled: Bool = true
+ 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
new file mode 100644
index 00000000..63e91260
--- /dev/null
+++ b/Sources/SwiftUICharts/Charts/PieChart/PieChart.swift
@@ -0,0 +1,17 @@
+import SwiftUI
+
+/// A type of chart that displays a slice of "pie" for each data point
+public struct PieChart: ChartBase {
+ public var chartData = ChartData()
+
+ @EnvironmentObject var style: ChartStyle
+
+ /// The content and behavior of the `PieChart`.
+ ///
+ ///
+ public var body: some View {
+ PieChartRow(chartData: chartData, style: style)
+ }
+
+ public init() {}
+}
diff --git a/Sources/SwiftUICharts/Charts/PieChart/PieChartCell.swift b/Sources/SwiftUICharts/Charts/PieChart/PieChartCell.swift
new file mode 100644
index 00000000..f733bcd7
--- /dev/null
+++ b/Sources/SwiftUICharts/Charts/PieChart/PieChartCell.swift
@@ -0,0 +1,116 @@
+import SwiftUI
+
+/// One slice of a `PieChartRow`
+struct PieSlice: Identifiable {
+ var id = UUID()
+ var startDeg: Double
+ var endDeg: Double
+ 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
+ var radius: CGFloat {
+ return min(rect.width, rect.height)/2
+ }
+ var startDeg: Double
+ var endDeg: Double
+
+ /// Path representing this slice
+ 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
+
+ /// 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
+ .fill(self.accentColor.linearGradient(from: .bottom, to: .top))
+ .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 {
+ 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: 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))
+ }.frame(width: 100, height: 100)
+
+ }.previewLayout(.fixed(width: 125, height: 125))
+ }
+}
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
new file mode 100644
index 00000000..f812a17e
--- /dev/null
+++ b/Sources/SwiftUICharts/Charts/PieChart/PieChartRow.swift
@@ -0,0 +1,69 @@
+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.points.reduce(0, +)
+
+ for slice in chartData.points {
+ let normalized: Double = Double(slice) / (maxValue == 0 ? 1 : maxValue)
+ let startDeg = lastEndDeg
+ let endDeg = lastEndDeg + (normalized * 360)
+ lastEndDeg = endDeg
+ tempSlices.append(PieSlice(startDeg: startDeg, endDeg: endDeg, value: slice))
+ }
+
+ return tempSlices
+ }
+
+ @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
+ })
+ )
+ }
+ }
+}
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..a46e0408
--- /dev/null
+++ b/Sources/SwiftUICharts/Charts/RingsChart/RingsChart.swift
@@ -0,0 +1,15 @@
+import SwiftUI
+
+public struct RingsChart: ChartBase {
+ public var chartData = 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: chartData, style: style)
+ }
+
+ public init() {}
+}
diff --git a/Sources/SwiftUICharts/Charts/RingsChart/RingsChartRow.swift b/Sources/SwiftUICharts/Charts/RingsChart/RingsChartRow.swift
new file mode 100644
index 00000000..7e2dd82b
--- /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.points[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)
+ }
+}
+
+
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/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/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.. [XCTestCaseEntry] {
return [
- testCase(SwiftUIChartsTests.allTests),
+ testCase(SwiftUIChartsTests.allTests)
]
}
#endif