diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..7f54e512
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,34 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+
+
+
+## Description
+
+
+## Expected Behavior
+
+
+## Actual Behavior
+
+
+## Possible Fix
+
+
+## Steps to Reproduce
+
+
+
+## Your Environment
+
+* Version of this package used:
+* Device/Simulator:
+* Operating System and version:
+* Link to your project:
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000..d4b1ee3d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Ask for a new feature
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+
+
+## Detailed Description
+
+
+## Context
+
+
+
+## Possible Implementation
+
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/v2Ticket.md b/.github/ISSUE_TEMPLATE/v2Ticket.md
new file mode 100644
index 00000000..ac9ec0cb
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/v2Ticket.md
@@ -0,0 +1,11 @@
+---
+name: v2 ticket
+about: Create tasks for the upcoming new version
+title: ''
+labels: v2
+assignees: ''
+
+---
+# v2 ticket
+
+## Ticket description:
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000..5eabcf1b
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,29 @@
+
+
+## Description
+
+
+## Motivation and Context
+
+
+
+## How Has This Been Tested?
+
+
+
+
+## Screenshots (if appropriate):
+
+## Types of changes
+
+- [ ] Bug fix (non-breaking change which fixes an issue)
+- [ ] New feature (non-breaking change which adds functionality)
+- [ ] Breaking change (fix or feature that would cause existing functionality to change)
+- [ ] Non-functional change (Updating Documentation, CI automation, etc..)
+
+## Checklist:
+
+
+- [ ] My code follows the code style of this project.
+- [ ] My change requires a change to the documentation.
+- [ ] I have updated the documentation accordingly.
diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml
new file mode 100644
index 00000000..ebe51e65
--- /dev/null
+++ b/.github/workflows/swift.yml
@@ -0,0 +1,22 @@
+name: Swift
+
+on:
+ push:
+ branches:
+ - master
+ - new-version
+ pull_request:
+ branches:
+ - master
+ - new-version
+jobs:
+ build:
+
+ runs-on: macos-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Build
+ run: swift build -v
+ - name: Run tests
+ run: swift test -v
diff --git a/.gitignore b/.gitignore
index 02c08753..0c62eec1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
/.build
/Packages
/*.xcodeproj
+.swiftpm
diff --git a/.swiftpm/xcode/package.xcworkspace/xcuserdata/andrassamu.xcuserdatad/UserInterfaceState.xcuserstate b/.swiftpm/xcode/package.xcworkspace/xcuserdata/andrassamu.xcuserdatad/UserInterfaceState.xcuserstate
index aae2473e..057e951c 100644
Binary files a/.swiftpm/xcode/package.xcworkspace/xcuserdata/andrassamu.xcuserdatad/UserInterfaceState.xcuserstate and b/.swiftpm/xcode/package.xcworkspace/xcuserdata/andrassamu.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/.swiftpm/xcode/package.xcworkspace/xcuserdata/roderic.xcuserdatad/UserInterfaceState.xcuserstate b/.swiftpm/xcode/package.xcworkspace/xcuserdata/roderic.xcuserdatad/UserInterfaceState.xcuserstate
new file mode 100644
index 00000000..aae6dfe7
Binary files /dev/null and b/.swiftpm/xcode/package.xcworkspace/xcuserdata/roderic.xcuserdatad/UserInterfaceState.xcuserstate differ
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..40db97a0
Binary files /dev/null and b/.swiftpm/xcode/package.xcworkspace/xcuserdata/samuandris.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcschemes/xcschememanagement.plist b/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 00000000..443dea97
--- /dev/null
+++ b/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,14 @@
+
+
+
+
+ SchemeUserState
+
+ SwiftUICharts.xcscheme_^#shared#^_
+
+ orderHint
+ 3
+
+
+
+
diff --git a/Package.swift b/Package.swift
index ffd10e06..293b294d 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:5.1
+// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "SwiftUICharts",
platforms: [
- .iOS(.v13),.watchOS(.v6)
+ .iOS(.v13), .watchOS(.v6), .macOS(.v11)
],
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
diff --git a/README.md b/README.md
index 23f773a2..dd022632 100644
--- a/README.md
+++ b/README.md
@@ -2,18 +2,43 @@
Swift package for displaying charts effortlessly.
-
+## V2 Beta is here 🎉🎉🎉
+
+V2 focuses on providing a strong and easy to use base, on which you can build your beautiful custom charts. It provides basic building blocks, like a chart view (bar, pie, line and ring chart), grid view, card view, interactive label for displaying the curent chart value.
+So you decide, whether you build a fully fledged interactive view, or just display a bare bone chart
+
+* [How to install SwiftUI ChartView](https://github.com/AppPear/ChartView/wiki/How-to-install-SwiftUI-ChartView)
+
+* [How to create your first chart](https://github.com/AppPear/ChartView/wiki/How-to-create-your-first-chart)
+
+### It supports interactions and animations
+
+
+### It is fully customizable, and works together with native SwiftUI elements well
+
+
+
+## Original (stable) version:
+
+
+
+### Usage
It supports:
* Line charts
* Bar charts
* Pie charts
+### Slack
+Join our Slack channel for day to day conversation and more insights:
+
+[Slack invite link](https://join.slack.com/t/swiftuichartview/shared_invite/zt-g6mxioq8-j3iUTF1YKX7D23ML3qcc4g)
+
### Installation:
-It requires iOS 13 and xCode 11!
+It requires iOS 13 and Xcode 11!
-In xCode got to `File -> Swift Packages -> Add Package Dependency` and paste inthe repo's url: `https://github.com/AppPear/ChartView`
+In Xcode go to `File -> Swift Packages -> Add Package Dependency` and paste in the repo's url: `https://github.com/AppPear/ChartView`
### Usage:
@@ -26,7 +51,47 @@ You can display a Chart by adding a chart view to your parent view:
Added an example project, with **iOS, watchOS** target: https://github.com/AppPear/ChartViewDemo
## Line charts
-
+
+**LineChartView with multiple lines!**
+First release of this feature, interaction is disabled for now, I'll figure it out how could be the best to interact with multiple lines with a single touch.
+
+
+
+Usage:
+```swift
+MultiLineChartView(data: [([8,32,11,23,40,28], GradientColors.green), ([90,99,78,111,70,60,77], GradientColors.purple), ([34,56,72,38,43,100,50], GradientColors.orngPink)], title: "Title")
+```
+Gradient colors are now under the `GradientColor` struct you can create your own gradient by `GradientColor(start: Color, end: Color)`
+
+Available preset gradients:
+* orange
+* blue
+* green
+* blu
+* bluPurpl
+* purple
+* prplPink
+* prplNeon
+* orngPink
+
+**Full screen view called LineView!!!**
+
+
+```swift
+ LineView(data: [8,23,54,32,12,37,7,23,43], title: "Line chart", legend: "Full screen") // legend is optional, use optional .padding()
+```
+
+Adopts to dark mode automatically
+
+
+
+You can add your custom darkmode style by specifying:
+
+```swift
+let myCustomStyle = ChartStyle(...)
+let myCutsomDarkModeStyle = ChartStyle(...)
+myCustomStyle.darkModeStyle = myCutsomDarkModeStyle
+```
**Line chart is interactive, so you can drag across to reveal the data points**
@@ -36,27 +101,61 @@ You can add a line chart with the following code:
LineChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", legend: "Legendary") // legend is optional
```
+**Turn drop shadow off by adding to the Initialiser: `dropShadow: false`**
+
## Bar charts
-
+
+**[New feature] you can display labels also along values and points for each bar to descirbe your data better!**
**Bar chart is interactive, so you can drag across to reveal the data points**
You can add a bar chart with the following code:
+Labels and points:
+
```swift
- BarChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", legend: "Legendary") // legend is optional
+ BarChartView(data: ChartData(values: [("2018 Q4",63150), ("2019 Q1",50900), ("2019 Q2",77550), ("2019 Q3",79600), ("2019 Q4",92550)]), title: "Sales", legend: "Quarterly") // legend is optional
```
+Only points:
+
+```swift
+ BarChartView(data: ChartData(points: [8,23,54,32,12,37,7,23,43]), title: "Title", legend: "Legendary") // legend is optional
+```
+
+**ChartData** structure
+Stores values in data pairs (actually tuple): `(String,Double)`
+* you can have duplicate values
+* keeps the data order
+
+You can initialise ChartData multiple ways:
+* For integer values: `ChartData(points: [8,23,54,32,12,37,7,23,43])`
+* For floating point values: `ChartData(points: [2.34,3.14,4.56])`
+* For label,value pairs: `ChartData(values: [("2018 Q4",63150), ("2019 Q1",50900)])`
+
You can add different formats:
-* Small `Form.small`
-* Medium `Form.medium`
-* Large `Form.large`
-
- ```swift
- BarChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", form: Form.small)
- ```
-
+* Small `ChartForm.small`
+* Medium `ChartForm.medium`
+* Large `ChartForm.large`
+
+```swift
+BarChartView(data: ChartData(points: [8,23,54,32,12,37,7,23,43]), title: "Title", form: ChartForm.small)
+```
+
+For floating point numbers, you can set a custom specifier:
+
+```swift
+BarChartView(data: ChartData(points:[1.23,2.43,3.37]) ,title: "A", valueSpecifier: "%.2f")
+```
+For integers you can disable by passing: `valueSpecifier: "%.0f"`
+
+
+You can set your custom image in the upper right corner by passing in the initialiser: `cornerImage:Image(systemName: "waveform.path.ecg")`
+
+
+ **Turn drop shadow off by adding to the Initialiser: `dropShadow: false`**
+
### You can customize styling of the chart with a ChartStyle object:
Customizable:
@@ -67,7 +166,7 @@ Customizable:
* legend text color
```swift
- let chartStyle = ChartStyle(backgroundColor: Color.black, accentColor: Colors.OrangeStart, secondGradientColor: Colors.OrangeEnd, chartFormSize: Form.medium, textColor: Color.white, legendTextColor: Color.white )
+ let chartStyle = ChartStyle(backgroundColor: Color.black, accentColor: Colors.OrangeStart, secondGradientColor: Colors.OrangeEnd, chartFormSize: ChartForm.medium, textColor: Color.white, legendTextColor: Color.white )
...
BarChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", style: chartStyle)
```
@@ -84,33 +183,38 @@ You can access built-in styles:
* barChartMidnightGreenLight
* barChartMidnightGreenDark
-
-
-
+
+
-### You can customize the size of the chart with a Form object:
+### You can customize the size of the chart with a ChartForm object:
-**Form**
+**ChartForm**
* `.small`
* `.medium`
* `.large`
* `.detail`
```swift
-BarChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", form: Form.small)
+BarChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", form: ChartForm.small)
```
+### You can choose whether bar is animated or not after completing your gesture.
+
+If you want to animate back movement after completing your gesture, you set `animatedToBack` as `true`.
+
### WatchOS support for Bar charts:
-
+
## Pie charts
-
+
-You can add a line chart with the following code:
+You can add a pie chart with the following code:
```swift
PieChartView(data: [8,23,54,32], title: "Title", legend: "Legendary") // legend is optional
```
+**Turn drop shadow off by adding to the Initialiser: `dropShadow: false`**
+
diff --git a/Sources/SwiftUICharts/BarChart/BarChartCell.swift b/Sources/SwiftUICharts/BarChart/BarChartCell.swift
index 1414ddb8..a3500b7f 100644
--- a/Sources/SwiftUICharts/BarChart/BarChartCell.swift
+++ b/Sources/SwiftUICharts/BarChart/BarChartCell.swift
@@ -17,19 +17,14 @@ public struct BarChartCell : View {
return Double(width)/(Double(numberOfDataPoints) * 1.5)
}
var accentColor: Color
- var secondGradientAccentColor: Color?
- var gradientColors:[Color] {
- if (secondGradientAccentColor != nil) {
- return [secondGradientAccentColor!, accentColor]
- }
- return [accentColor, accentColor]
- }
+ 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(colors: gradientColors), startPoint: .bottom, endPoint: .top))
+ .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)
@@ -43,7 +38,7 @@ public struct BarChartCell : View {
#if DEBUG
struct ChartCell_Previews : PreviewProvider {
static var previews: some View {
- BarChartCell(value: Double(0.75), width: 320, numberOfDataPoints: 12, accentColor: Colors.OrangeStart, secondGradientAccentColor: nil, touchLocation: .constant(-1))
+ 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
index 242f4efd..0ccd935e 100644
--- a/Sources/SwiftUICharts/BarChart/BarChartRow.swift
+++ b/Sources/SwiftUICharts/BarChart/BarChartRow.swift
@@ -9,19 +9,30 @@
import SwiftUI
public struct BarChartRow : View {
- var data: [Int]
+ var data: [Double]
var accentColor: Color
- var secondGradientAccentColor: Color?
- var maxValue: Int {
- data.max() ?? 0
+ var gradient: GradientColor?
+
+ var maxValue: Double {
+ guard let max = data.max() else {
+ return 1
+ }
+ return max != 0 ? max : 1
}
@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())
}
}
@@ -37,7 +48,10 @@ public struct BarChartRow : View {
#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))
+ Group {
+ BarChartRow(data: [0], accentColor: Colors.OrangeStart, touchLocation: .constant(-1))
+ 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
index d6013c7e..6a502e7c 100644
--- a/Sources/SwiftUICharts/BarChart/BarChartView.swift
+++ b/Sources/SwiftUICharts/BarChart/BarChartView.swift
@@ -9,98 +9,152 @@
import SwiftUI
public struct BarChartView : View {
- public var data: [Int]
+ @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
-// let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
+ public var dropShadow: Bool
+ public var cornerImage: Image?
+ public var valueSpecifier:String
+ public var animatedToBack: Bool
@State private var touchLocation: CGFloat = -1.0
@State private var showValue: Bool = false
- @State private var currentValue: Int = 0 {
+ @State private var showLabelValue: Bool = false
+ @State private var currentValue: Double = 0 {
didSet{
if(oldValue != self.currentValue && self.showValue) {
-// selectionFeedbackGenerator.selectionChanged()
HapticFeedback.playSelection()
}
}
}
var isFullWidth:Bool {
- return self.formSize == Form.large
+ return self.formSize == ChartForm.large
}
- public init(data: [Int], title: String, legend: String? = nil, style: ChartStyle = Styles.barChartStyleOrangeLight, form: CGSize? = Form.medium){
+ 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", animatedToBack: Bool = false){
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!
+ self.animatedToBack = animatedToBack
}
public var body: some View {
ZStack{
Rectangle()
- .fill(self.style.backgroundColor)
+ .fill(self.colorScheme == .dark ? self.darkModeStyle.backgroundColor : self.style.backgroundColor)
.cornerRadius(20)
- .shadow(color: Color.gray, radius: 8 )
+ .shadow(color: self.style.dropShadowColor, radius: self.dropShadow ? 8 : 0)
VStack(alignment: .leading){
HStack{
if(!showValue){
Text(self.title)
.font(.headline)
- .foregroundColor(self.style.textColor)
+ .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor)
}else{
- Text("\(self.currentValue)")
+ Text("\(self.currentValue, specifier: self.valueSpecifier)")
.font(.headline)
- .foregroundColor(self.style.textColor)
+ .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor)
}
- if(self.formSize == Form.large && self.legend != nil && !showValue) {
+ if(self.formSize == ChartForm.large && self.legend != nil && !showValue) {
Text(self.legend!)
.font(.callout)
- .foregroundColor(self.style.accentColor)
+ .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.accentColor : self.style.accentColor)
.transition(.opacity)
.animation(.easeOut)
}
Spacer()
- Image(systemName: "waveform.path.ecg")
+ self.cornerImage
.imageScale(.large)
- .foregroundColor(self.style.legendTextColor)
+ .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor)
}.padding()
- BarChartRow(data: data, accentColor: self.style.accentColor, secondGradientAccentColor: self.style.secondGradientColor, touchLocation: self.$touchLocation)
- if self.legend != nil && self.formSize == Form.medium {
+ 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.style.legendTextColor)
+ .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)
+ }.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()
+ self.currentValue = self.getCurrentValue()?.1 ?? 0
+ if(self.data.valuesGiven && self.formSize == ChartForm.medium) {
+ self.showLabelValue = true
+ }
})
.onEnded({ value in
- self.showValue = false
- self.touchLocation = -1
+ if animatedToBack {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
+ withAnimation(Animation.easeOut(duration: 1)) {
+ self.showValue = false
+ self.showLabelValue = false
+ self.touchLocation = -1
+ }
+ }
+ } else {
+ self.showValue = false
+ self.showLabelValue = false
+ self.touchLocation = -1
+ }
})
)
.gesture(TapGesture()
)
}
- func getCurrentValue()-> Int{
- let index = max(0,min(self.data.count-1,Int(floor((self.touchLocation*self.formSize.width)/(self.formSize.width/CGFloat(self.data.count))))))
- return self.data[index]
+ 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: [8,23,54,32,12,37,7,23,43], title: "Title", legend: "Legendary")
+ 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
new file mode 100644
index 00000000..f17ae7be
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/LabelView.swift
@@ -0,0 +1,46 @@
+//
+// 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/Helpers.swift b/Sources/SwiftUICharts/Helpers.swift
index 9f7e9293..5198c841 100644
--- a/Sources/SwiftUICharts/Helpers.swift
+++ b/Sources/SwiftUICharts/Helpers.swift
@@ -1,6 +1,6 @@
//
// File.swift
-//
+//
//
// Created by András Samu on 2019. 07. 19..
//
@@ -15,10 +15,11 @@ public struct Colors {
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 OrangeStart:Color = Color(hexString: "#FF782C")
- public static let OrangeEnd:Color = Color(hexString: "#EC2301")
+ 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")
@@ -29,115 +30,216 @@ public struct Colors {
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)
+ 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)
+ 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)
+ 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)
+ 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)
+ 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"))
+ 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)
+ 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.black,
- legendTextColor: Color.gray)
+ textColor: Color.white,
+ legendTextColor: Color.white,
+ dropShadowColor: Color.gray)
}
-public struct Form {
+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 extraLarge = 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 extraLarge = CGSize(width:360, height:240)
public static let detail = CGSize(width:180, height:120)
#endif
-
-
}
-public struct ChartStyle {
+public class ChartStyle {
public var backgroundColor: Color
public var accentColor: Color
- public var secondGradientColor: 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){
+ public init(backgroundColor: Color, accentColor: Color, secondGradientColor: Color, textColor: Color, legendTextColor: Color, dropShadowColor: Color){
self.backgroundColor = backgroundColor
self.accentColor = accentColor
- self.secondGradientColor = secondGradientColor
+ 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.secondGradientColor = Colors.OrangeEnd
+ 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 }
}
}
-class ChartData: ObservableObject {
- @Published var points: [Int] = [Int]()
- @Published var currentPoint: Int? = nil
+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)
+ }
- init(points:[Int]) {
- self.points = points
+ public func getGradient() -> GradientColor {
+ return self.gradient
}
}
-class TestData{
+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 {
@@ -166,11 +268,15 @@ class HapticFeedback {
static func playSelection() -> Void {
WKInterfaceDevice.current().play(.click)
}
- #else
+ #elseif os(iOS)
//iOS implementation
let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
static func playSelection() -> Void {
UISelectionFeedbackGenerator().selectionChanged()
}
+ #else
+ static func playSelection() -> Void {
+ //No-op
+ }
#endif
}
diff --git a/Sources/SwiftUICharts/LineChart/Legend.swift b/Sources/SwiftUICharts/LineChart/Legend.swift
index a706c0d5..10190810 100644
--- a/Sources/SwiftUICharts/LineChart/Legend.swift
+++ b/Sources/SwiftUICharts/LineChart/Legend.swift
@@ -12,23 +12,42 @@ struct Legend: View {
@ObservedObject var data: ChartData
@Binding var frame: CGRect
@Binding var hideHorizontalLines: Bool
-
+ @Environment(\.colorScheme) var colorScheme: ColorScheme
+ var specifier: String = "%.2f"
+ 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 {
- return frame.size.height / CGFloat(data.points.max()! + data.points.min()!)
+ 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.getYLegend()![height])").offset(x: 0, y: (self.frame.height-CGFloat(self.getYLegend()![height])*self.stepHeight)-(self.frame.height/2))
+ Text("\(self.getYLegendSafe(height: height), specifier: specifier)").offset(x: 0, y: self.getYposition(height: height) )
.foregroundColor(Colors.LegendText)
.font(.caption)
- self.line(atHeight: CGFloat(self.getYLegend()![height]), width: self.frame.width)
- .stroke(Colors.LegendColor, style: StrokeStyle(lineWidth: 1.5, lineCap: .round, dash: [5,height == 0 ? 0 : 10]))
+ 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))
@@ -41,30 +60,41 @@ struct Legend: View {
}
}
+ 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*stepHeight))
- hLine.addLine(to: CGPoint(x: width, y: atHeight*stepHeight))
+ hLine.move(to: CGPoint(x:5, y: (atHeight-min)*stepHeight))
+ hLine.addLine(to: CGPoint(x: width, y: (atHeight-min)*stepHeight))
return hLine
}
- func getYLegend() -> [Int]? {
- guard let max = data.points.max() else { return nil }
- guard let min = data.points.min() else { return nil }
- if(min > 0){
- let upperBound = ((max/10)+1) * 10
- let step = upperBound/4
- return [step * 0,step * 1, step * 2, step * 3, step * 4]
- }
-
- return nil
+ 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: TestData.data, frame: .constant(geometry.frame(in: .local)), hideHorizontalLines: .constant(false))
+ 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
index 363c4085..d29c1874 100644
--- a/Sources/SwiftUICharts/LineChart/Line.swift
+++ b/Sources/SwiftUICharts/LineChart/Line.swift
@@ -8,28 +8,57 @@
import SwiftUI
-struct Line: View {
+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 {
- return frame.size.height / CGFloat(data.points.max()! + data.points.min()!)
+ var min: Double?
+ var max: Double?
+ let points = self.data.onlyPoints()
+ if minDataValue != nil && maxDataValue != nil {
+ min = minDataValue!
+ max = maxDataValue!
+ }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 {
- return Path.quadCurvedPathWithPoints(points: data.points, step: CGPoint(x: stepWidth, y: stepHeight))
+ 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 {
- return Path.quadClosedCurvedPathWithPoints(points: data.points, step: CGPoint(x: stepWidth, y: stepHeight))
+ 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))
}
- var body: some View {
+ public var body: some View {
ZStack {
if(self.showFull && self.showBackground){
self.closedPath
@@ -41,13 +70,16 @@ struct Line: View {
}
self.path
.trim(from: 0, to: self.showFull ? 1:0)
- .stroke(LinearGradient(gradient: Gradient(colors: [Colors.GradientPurple, Colors.GradientNeonBlue]), startPoint: .leading, endPoint: .trailing) ,style: StrokeStyle(lineWidth: 3))
+ .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(.easeOut(duration: 1.2))
- .onAppear(){
- self.showFull.toggle()
- }.drawingGroup()
+ .animation(Animation.easeOut(duration: 1.2).delay(Double(self.index)*0.4))
+ .onAppear {
+ self.showFull = true
+ }
+ .onDisappear {
+ self.showFull = false
+ }
if(self.showIndicator) {
IndicatorPoint()
.position(self.getClosestPointOnPath(touchLocation: self.touchLocation))
@@ -58,102 +90,16 @@ struct Line: View {
}
func getClosestPointOnPath(touchLocation: CGPoint) -> CGPoint {
- let percentage:CGFloat = min(max(touchLocation.x,0)/self.frame.width,1)
- let closest = self.path.percentPoint(percentage)
+ let closest = self.path.point(to: touchLocation.x)
return closest
}
}
-extension CGPoint {
- static func getMidPoint(point1: CGPoint, point2: CGPoint) -> CGPoint {
- return CGPoint(
- x: point1.x + (point2.x - point1.x) / 2,
- y: point1.y + (point2.y - point1.y) / 2
- )
- }
-
- func dist(to: CGPoint) -> CGFloat {
- return sqrt((pow(self.x - to.x, 2) + pow(self.y - to.y, 2)))
- }
-
- static func midPointForPoints(p1:CGPoint, p2:CGPoint) -> CGPoint {
- return CGPoint(x:(p1.x + p2.x) / 2,y: (p1.y + p2.y) / 2)
- }
-
- static func controlPointForPoints(p1:CGPoint, p2:CGPoint) -> CGPoint {
- var controlPoint = CGPoint.midPointForPoints(p1:p1, p2:p2)
- let diffY = abs(p2.y - controlPoint.y)
-
- if (p1.y < p2.y){
- controlPoint.y += diffY
- } else if (p1.y > p2.y) {
- controlPoint.y -= diffY
- }
- return controlPoint
- }
-}
-extension Path {
- static func quadCurvedPathWithPoints(points:[Int], step:CGPoint) -> Path {
- var path = Path()
- var p1 = CGPoint(x: 0, y: CGFloat(points[0])*step.y)
- path.move(to: p1)
- if(points.count < 2){
- path.addLine(to: CGPoint(x: step.x, y: step.y*CGFloat(points[1])))
- return path
- }
- for pointIndex in 1.. Path {
- var path = Path()
- path.move(to: .zero)
- var p1 = CGPoint(x: 0, y: CGFloat(points[0])*step.y)
- path.addLine(to: p1)
- if(points.count < 2){
- path.addLine(to: CGPoint(x: step.x, y: step.y*CGFloat(points[1])))
- return path
- }
- for pointIndex in 1.. CGPoint {
- // percent difference between points
- let diff: CGFloat = 0.001
- let comp: CGFloat = 1 - diff
-
- // handle limits
- let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent)
-
- let f = pct > comp ? comp : pct
- let t = pct > comp ? 1 : pct + diff
- let tp = self.trimmedPath(from: f, to: t)
-
- return CGPoint(x: tp.boundingRect.midX, y: tp.boundingRect.midY)
- }
-
-}
-
struct Line_Previews: PreviewProvider {
static var previews: some View {
GeometryReader{ geometry in
- Line(data: TestData.data, frame: .constant(geometry.frame(in: .local)), touchLocation: .constant(CGPoint(x: 300, y: 12)), showIndicator: .constant(true))
+ 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
index 3cd0580c..8e48faa1 100644
--- a/Sources/SwiftUICharts/LineChart/LineChartView.swift
+++ b/Sources/SwiftUICharts/LineChart/LineChartView.swift
@@ -9,46 +9,80 @@
import SwiftUI
public struct LineChartView: View {
-// let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
+ @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: Int = 2 {
+ @State private var currentValue: Double = 2 {
didSet{
if (oldValue != self.currentValue && showIndicatorDot) {
-// selectionFeedbackGenerator.selectionChanged()
HapticFeedback.playSelection()
}
}
}
- let frame = CGSize(width: 180, height: 120)
+ var frame = CGSize(width: 180, height: 120)
+ private var rateValue: Int?
- public init(data: [Int], title: String, legend: String? = nil, style: ChartStyle = Styles.lineChartStyleOne, form: CGSize? = Form.medium){
+ public init(data: [Double],
+ title: String,
+ legend: String? = nil,
+ style: ChartStyle = Styles.lineChartStyleOne,
+ form: CGSize? = ChartForm.medium,
+ rateValue: Int?,
+ 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!
+ frame = CGSize(width: self.formSize.width, height: self.formSize.height/2)
+ self.dropShadow = dropShadow!
+ self.valueSpecifier = valueSpecifier!
+ self.rateValue = rateValue
}
public var body: some View {
ZStack(alignment: .center){
- RoundedRectangle(cornerRadius: 20).fill(self.style.backgroundColor).frame(width: frame.width, height: 240, alignment: .center).shadow(radius: 8)
+ 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.style.textColor)
+ 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.style.legendTextColor)
+ Text(self.legend!)
+ .font(.callout)
+ .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor :self.style.legendTextColor)
}
HStack {
- Image(systemName: "arrow.up")
- Text("14%")
+
+ if let rateValue = self.rateValue
+ {
+ if (rateValue ?? 0 >= 0){
+ Image(systemName: "arrow.up")
+ }else{
+ Image(systemName: "arrow.down")
+ }
+ Text("\(rateValue!)%")
+ }
}
}
.transition(.opacity)
@@ -57,18 +91,22 @@ public struct LineChartView: View {
}else{
HStack{
Spacer()
- Text("\(self.currentValue)")
+ Text("\(self.currentValue, specifier: self.valueSpecifier)")
.font(.system(size: 41, weight: .bold, design: .default))
.offset(x: 0, y: 30)
Spacer()
}
.transition(.scale)
- .animation(.spring())
-
}
Spacer()
GeometryReader{ geometry in
- Line(data: self.data, frame: .constant(geometry.frame(in: .local)), touchLocation: self.$touchLocation, showIndicator: self.$showIndicatorDot)
+ 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)
.clipShape(RoundedRectangle(cornerRadius: 20))
@@ -87,14 +125,15 @@ public struct LineChartView: View {
)
}
- func getClosestDataPoint(toPoint: CGPoint, width:CGFloat, height: CGFloat) -> CGPoint {
- let stepWidth: CGFloat = width / CGFloat(data.points.count-1)
- let stepHeight: CGFloat = height / CGFloat(data.points.max()! + data.points.min()!)
+ @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 < data.points.count){
- self.currentValue = self.data.points[index]
- return CGPoint(x: CGFloat(index)*stepWidth, y: CGFloat(self.data.points[index])*stepHeight)
+ if (index >= 0 && index < points.count){
+ self.currentValue = points[index]
+ return CGPoint(x: CGFloat(index)*stepWidth, y: CGFloat(points[index])*stepHeight)
}
return .zero
}
@@ -105,6 +144,9 @@ struct WidgetView_Previews: PreviewProvider {
Group {
LineChartView(data: [8,23,54,32,12,37,7,23,43], title: "Line chart", legend: "Basic")
.environment(\.colorScheme, .light)
+
+ LineChartView(data: [282.502, 284.495, 283.51, 285.019, 285.197, 286.118, 288.737, 288.455, 289.391, 287.691, 285.878, 286.46, 286.252, 284.652, 284.129, 284.188], title: "Line chart", legend: "Basic")
+ .environment(\.colorScheme, .light)
}
}
}
diff --git a/Sources/SwiftUICharts/LineChart/LineView.swift b/Sources/SwiftUICharts/LineChart/LineView.swift
new file mode 100644
index 00000000..c69434a7
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/LineView.swift
@@ -0,0 +1,135 @@
+//
+// 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
+ public var legendSpecifier: 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",
+ legendSpecifier: String? = "%.2f") {
+
+ self.data = ChartData(points: data)
+ self.title = title
+ self.legend = legend
+ self.style = style
+ self.valueSpecifier = valueSpecifier!
+ self.legendSpecifier = legendSpecifier!
+ 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, specifier: legendSpecifier)
+ .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 + 25)),
+ 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 {
+ Group {
+ LineView(data: [8,23,54,32,12,37,7,23,43], title: "Full chart", style: Styles.lineChartStyleOne)
+
+ LineView(data: [282.502, 284.495, 283.51, 285.019, 285.197, 286.118, 288.737, 288.455, 289.391, 287.691, 285.878, 286.46, 286.252, 284.652, 284.129, 284.188], title: "Full chart", style: Styles.lineChartStyleOne)
+
+ }
+ }
+}
+
diff --git a/Sources/SwiftUICharts/LineChart/MagnifierRect.swift b/Sources/SwiftUICharts/LineChart/MagnifierRect.swift
new file mode 100644
index 00000000..479f0bf6
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/MagnifierRect.swift
@@ -0,0 +1,34 @@
+//
+// 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)
+ }
+ }
+ .offset(x: 0, y: -15)
+ }
+}
diff --git a/Sources/SwiftUICharts/LineChart/MultiLineChartView.swift b/Sources/SwiftUICharts/LineChart/MultiLineChartView.swift
new file mode 100644
index 00000000..93a5f6fb
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/MultiLineChartView.swift
@@ -0,0 +1,164 @@
+//
+// 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
+ }
+
+ var 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? = nil,
+ 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
+ frame = CGSize(width: self.formSize.width, height: self.formSize.height/2)
+ 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 (rateValue ?? 0 >= 0){
+ Image(systemName: "arrow.up")
+ }else{
+ Image(systemName: "arrow.down")
+ }
+ Text("\(rateValue ?? 0)%")
+ }
+ }
+ .transition(.opacity)
+ .animation(.easeIn(duration: 0.1))
+ .padding([.leading, .top])
+ }else{
+ HStack{
+ Spacer()
+ Text("\(self.currentValue, specifier: self.valueSpecifier)")
+ .font(.system(size: 41, weight: .bold, design: .default))
+ .offset(x: 0, y: 30)
+ Spacer()
+ }
+ .transition(.scale)
+ }
+ Spacer()
+ GeometryReader{ geometry in
+ ZStack{
+ ForEach(0.. CGPoint {
+// let points = self.data.onlyPoints()
+// let stepWidth: CGFloat = width / CGFloat(points.count-1)
+// let stepHeight: CGFloat = height / CGFloat(points.max()! + points.min()!)
+//
+// let index:Int = Int(round((toPoint.x)/stepWidth))
+// if (index >= 0 && index < points.count){
+// self.currentValue = points[index]
+// return CGPoint(x: CGFloat(index)*stepWidth, y: CGFloat(points[index])*stepHeight)
+// }
+// return .zero
+// }
+}
+
+struct MultiWidgetView_Previews: PreviewProvider {
+ static var previews: some View {
+ Group {
+ MultiLineChartView(data: [([8,23,54,32,12,37,7,23,43], GradientColors.orange)], title: "Line chart", legend: "Basic")
+ .environment(\.colorScheme, .light)
+ }
+ }
+}
diff --git a/Sources/SwiftUICharts/LineChart/Path+QuadCurve.swift b/Sources/SwiftUICharts/LineChart/Path+QuadCurve.swift
new file mode 100644
index 00000000..83cf114b
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Path+QuadCurve.swift
@@ -0,0 +1,353 @@
+//
+// File.swift
+//
+//
+// Created by xspyhack on 2020/1/21.
+//
+
+import SwiftUI
+
+extension Path {
+ func trimmedPath(for percent: CGFloat) -> Path {
+ // percent difference between points
+ let boundsDistance: CGFloat = 0.001
+ let completion: CGFloat = 1 - boundsDistance
+
+ let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent)
+
+ let start = pct > completion ? completion : pct - boundsDistance
+ let end = pct > completion ? 1 : pct + boundsDistance
+ return trimmedPath(from: start, to: end)
+ }
+
+ func point(for percent: CGFloat) -> CGPoint {
+ let path = trimmedPath(for: percent)
+ return CGPoint(x: path.boundingRect.midX, y: path.boundingRect.midY)
+ }
+
+ func point(to maxX: CGFloat) -> CGPoint {
+ let total = length
+ let sub = length(to: maxX)
+ let percent = sub / total
+ return point(for: percent)
+ }
+
+ var length: CGFloat {
+ var ret: CGFloat = 0.0
+ var start: CGPoint?
+ var point = CGPoint.zero
+
+ forEach { ele in
+ switch ele {
+ case .move(let to):
+ if start == nil {
+ start = to
+ }
+ point = to
+ case .line(let to):
+ ret += point.line(to: to)
+ point = to
+ case .quadCurve(let to, let control):
+ ret += point.quadCurve(to: to, control: control)
+ point = to
+ case .curve(let to, let control1, let control2):
+ ret += point.curve(to: to, control1: control1, control2: control2)
+ point = to
+ case .closeSubpath:
+ if let to = start {
+ ret += point.line(to: to)
+ point = to
+ }
+ start = nil
+ }
+ }
+ return ret
+ }
+
+ func length(to maxX: CGFloat) -> CGFloat {
+ var ret: CGFloat = 0.0
+ var start: CGPoint?
+ var point = CGPoint.zero
+ var finished = false
+
+ forEach { ele in
+ if finished {
+ return
+ }
+ switch ele {
+ case .move(let to):
+ if to.x > maxX {
+ finished = true
+ return
+ }
+ if start == nil {
+ start = to
+ }
+ point = to
+ case .line(let to):
+ if to.x > maxX {
+ finished = true
+ ret += point.line(to: to, x: maxX)
+ return
+ }
+ ret += point.line(to: to)
+ point = to
+ case .quadCurve(let to, let control):
+ if to.x > maxX {
+ finished = true
+ ret += point.quadCurve(to: to, control: control, x: maxX)
+ return
+ }
+ ret += point.quadCurve(to: to, control: control)
+ point = to
+ case .curve(let to, let control1, let control2):
+ if to.x > maxX {
+ finished = true
+ ret += point.curve(to: to, control1: control1, control2: control2, x: maxX)
+ return
+ }
+ ret += point.curve(to: to, control1: control1, control2: control2)
+ point = to
+ case .closeSubpath:
+ fatalError("Can't include closeSubpath")
+ }
+ }
+ return ret
+ }
+
+ static func quadCurvedPathWithPoints(points:[Double], step:CGPoint, globalOffset: Double? = nil) -> Path {
+ var path = Path()
+ if (points.count < 2){
+ return path
+ }
+ let offset = globalOffset ?? points.min()!
+// guard let offset = points.min() else { return path }
+ var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
+ path.move(to: p1)
+ for pointIndex in 1.. Path {
+ var path = Path()
+ if (points.count < 2){
+ return path
+ }
+ let offset = globalOffset ?? points.min()!
+
+// guard let offset = points.min() else { return path }
+ path.move(to: .zero)
+ var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
+ path.addLine(to: p1)
+ for pointIndex in 1.. Path {
+ var path = Path()
+ if (points.count < 2){
+ return path
+ }
+ guard let offset = points.min() else { return path }
+ let p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
+ path.move(to: p1)
+ for pointIndex in 1.. Path {
+ var path = Path()
+ if (points.count < 2){
+ return path
+ }
+ guard let offset = points.min() else { return path }
+ var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
+ path.move(to: p1)
+ for pointIndex in 1.. CGPoint {
+ let a = (to.y - self.y) / (to.x - self.x)
+ let y = self.y + (x - self.x) * a
+ return CGPoint(x: x, y: y)
+ }
+
+ func line(to: CGPoint) -> CGFloat {
+ dist(to: to)
+ }
+
+ func line(to: CGPoint, x: CGFloat) -> CGFloat {
+ dist(to: point(to: to, x: x))
+ }
+
+ func quadCurve(to: CGPoint, control: CGPoint) -> CGFloat {
+ var dist: CGFloat = 0
+ let steps: CGFloat = 100
+
+ for i in 0.. CGFloat {
+ var dist: CGFloat = 0
+ let steps: CGFloat = 100
+
+ for i in 0..= x {
+ return dist
+ } else if b.x > x {
+ dist += a.line(to: b, x: x)
+ return dist
+ } else if b.x == x {
+ dist += a.line(to: b)
+ return dist
+ }
+
+ dist += a.line(to: b)
+ }
+ return dist
+ }
+
+ func point(to: CGPoint, t: CGFloat, control: CGPoint) -> CGPoint {
+ let x = CGPoint.value(x: self.x, y: to.x, t: t, c: control.x)
+ let y = CGPoint.value(x: self.y, y: to.y, t: t, c: control.y)
+
+ return CGPoint(x: x, y: y)
+ }
+
+ func curve(to: CGPoint, control1: CGPoint, control2: CGPoint) -> CGFloat {
+ var dist: CGFloat = 0
+ let steps: CGFloat = 100
+
+ for i in 0.. CGFloat {
+ var dist: CGFloat = 0
+ let steps: CGFloat = 100
+
+ for i in 0..= x {
+ return dist
+ } else if b.x > x {
+ dist += a.line(to: b, x: x)
+ return dist
+ } else if b.x == x {
+ dist += a.line(to: b)
+ return dist
+ }
+
+ dist += a.line(to: b)
+ }
+
+ return dist
+ }
+
+ func point(to: CGPoint, t: CGFloat, control1: CGPoint, control2: CGPoint) -> CGPoint {
+ let x = CGPoint.value(x: self.x, y: to.x, t: t, c1: control1.x, c2: control2.x)
+ let y = CGPoint.value(x: self.y, y: to.y, t: t, c1: control1.y, c2: control2.x)
+
+ return CGPoint(x: x, y: y)
+ }
+
+ static func value(x: CGFloat, y: CGFloat, t: CGFloat, c: CGFloat) -> CGFloat {
+ var value: CGFloat = 0.0
+ // (1-t)^2 * p0 + 2 * (1-t) * t * c1 + t^2 * p1
+ value += pow(1-t, 2) * x
+ value += 2 * (1-t) * t * c
+ value += pow(t, 2) * y
+ return value
+ }
+
+ static func value(x: CGFloat, y: CGFloat, t: CGFloat, c1: CGFloat, c2: CGFloat) -> CGFloat {
+ var value: CGFloat = 0.0
+ // (1-t)^3 * p0 + 3 * (1-t)^2 * t * c1 + 3 * (1-t) * t^2 * c2 + t^3 * p1
+ value += pow(1-t, 3) * x
+ value += 3 * pow(1-t, 2) * t * c1
+ value += 3 * (1-t) * pow(t, 2) * c2
+ value += pow(t, 3) * y
+ return value
+ }
+
+ static func getMidPoint(point1: CGPoint, point2: CGPoint) -> CGPoint {
+ return CGPoint(
+ x: point1.x + (point2.x - point1.x) / 2,
+ y: point1.y + (point2.y - point1.y) / 2
+ )
+ }
+
+ func dist(to: CGPoint) -> CGFloat {
+ return sqrt((pow(self.x - to.x, 2) + pow(self.y - to.y, 2)))
+ }
+
+ static func midPointForPoints(p1:CGPoint, p2:CGPoint) -> CGPoint {
+ return CGPoint(x:(p1.x + p2.x) / 2,y: (p1.y + p2.y) / 2)
+ }
+
+ static func controlPointForPoints(p1:CGPoint, p2:CGPoint) -> CGPoint {
+ var controlPoint = CGPoint.midPointForPoints(p1:p1, p2:p2)
+ let diffY = abs(p2.y - controlPoint.y)
+
+ if (p1.y < p2.y){
+ controlPoint.y += diffY
+ } else if (p1.y > p2.y) {
+ controlPoint.y -= diffY
+ }
+ return controlPoint
+ }
+}
+
diff --git a/Sources/SwiftUICharts/PieChart/PieChartCell.swift b/Sources/SwiftUICharts/PieChart/PieChartCell.swift
index 352ce6b7..f511165e 100644
--- a/Sources/SwiftUICharts/PieChart/PieChartCell.swift
+++ b/Sources/SwiftUICharts/PieChart/PieChartCell.swift
@@ -12,7 +12,7 @@ struct PieSlice: Identifiable {
var id = UUID()
var startDeg: Double
var endDeg: Double
- var value: Int
+ var value: Double
var normalizedValue: Double
}
diff --git a/Sources/SwiftUICharts/PieChart/PieChartHelpers.swift b/Sources/SwiftUICharts/PieChart/PieChartHelpers.swift
new file mode 100644
index 00000000..2c2af8b1
--- /dev/null
+++ b/Sources/SwiftUICharts/PieChart/PieChartHelpers.swift
@@ -0,0 +1,41 @@
+//
+// File.swift
+//
+//
+// Created by 曾文志 on 2020/7/30.
+//
+
+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/PieChart/PieChartRow.swift b/Sources/SwiftUICharts/PieChart/PieChartRow.swift
index 354ebfff..a462cd96 100644
--- a/Sources/SwiftUICharts/PieChart/PieChartRow.swift
+++ b/Sources/SwiftUICharts/PieChart/PieChartRow.swift
@@ -9,7 +9,7 @@
import SwiftUI
public struct PieChartRow : View {
- var data: [Int]
+ var data: [Double]
var backgroundColor: Color
var accentColor: Color
var slices: [PieSlice] {
@@ -25,13 +25,42 @@ public struct PieChartRow : View {
}
return tempSlices
}
+
+ @Binding var showValue: Bool
+ @Binding var currentValue: Double
+
+ @State private var currentTouchedIndex = -1 {
+ didSet {
+ if oldValue != currentTouchedIndex {
+ showValue = currentTouchedIndex != -1
+ currentValue = showValue ? slices[currentTouchedIndex].value : 0
+ }
+ }
+ }
+
public var body: some View {
GeometryReader { geometry in
ZStack{
ForEach(0.. touchDegree }) ?? -1
+ } else {
+ self.currentTouchedIndex = -1
+ }
+ })
+ .onEnded({ value in
+ self.currentTouchedIndex = -1
+ }))
}
}
}
@@ -39,8 +68,12 @@ public struct PieChartRow : View {
#if DEBUG
struct PieChartRow_Previews : PreviewProvider {
static var previews: some View {
- PieChartRow(data:[8,23,54,32,12,37,7,23,43], 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)
-
+ Group {
+ PieChartRow(data:[8,23,54,32,12,37,7,23,43], 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), showValue: Binding.constant(false), currentValue: Binding.constant(0))
+ .frame(width: 100, height: 100)
+ PieChartRow(data:[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), showValue: Binding.constant(false), currentValue: Binding.constant(0))
+ .frame(width: 100, height: 100)
+ }
}
}
#endif
diff --git a/Sources/SwiftUICharts/PieChart/PieChartView.swift b/Sources/SwiftUICharts/PieChart/PieChartView.swift
index 59a06aed..81609bd1 100644
--- a/Sources/SwiftUICharts/PieChart/PieChartView.swift
+++ b/Sources/SwiftUICharts/PieChart/PieChartView.swift
@@ -9,17 +9,34 @@
import SwiftUI
public struct PieChartView : View {
- public var data: [Int]
+ public var data: [Double]
public var title: String
public var legend: String?
public var style: ChartStyle
public var formSize:CGSize
- public init(data: [Int], title: String, legend: String? = nil, style: ChartStyle = Styles.pieChartStyleOne, form: CGSize? = Form.medium ){
+ public var dropShadow: Bool
+ public var valueSpecifier:String
+
+ @State private var showValue = false
+ @State private var currentValue: Double = 0 {
+ didSet{
+ if(oldValue != self.currentValue && self.showValue) {
+ HapticFeedback.playSelection()
+ }
+ }
+ }
+
+ public init(data: [Double], title: String, legend: String? = nil, style: ChartStyle = Styles.pieChartStyleOne, form: CGSize? = ChartForm.medium, dropShadow: Bool? = true, valueSpecifier: String? = "%.1f"){
self.data = data
self.title = title
self.legend = legend
self.style = style
self.formSize = form!
+ if self.formSize == ChartForm.large {
+ self.formSize = ChartForm.extraLarge
+ }
+ self.dropShadow = dropShadow!
+ self.valueSpecifier = valueSpecifier!
}
public var body: some View {
@@ -27,18 +44,24 @@ public struct PieChartView : View {
Rectangle()
.fill(self.style.backgroundColor)
.cornerRadius(20)
- .shadow(color: Color.gray, radius: 12)
+ .shadow(color: self.style.dropShadowColor, radius: self.dropShadow ? 12 : 0)
VStack(alignment: .leading){
HStack{
- Text(self.title)
- .font(.headline)
- .foregroundColor(self.style.textColor)
+ if(!showValue){
+ Text(self.title)
+ .font(.headline)
+ .foregroundColor(self.style.textColor)
+ }else{
+ Text("\(self.currentValue, specifier: self.valueSpecifier)")
+ .font(.headline)
+ .foregroundColor(self.style.textColor)
+ }
Spacer()
Image(systemName: "chart.pie.fill")
.imageScale(.large)
.foregroundColor(self.style.legendTextColor)
}.padding()
- PieChartRow(data: data, backgroundColor: self.style.backgroundColor, accentColor: self.style.accentColor)
+ PieChartRow(data: data, backgroundColor: self.style.backgroundColor, accentColor: self.style.accentColor, showValue: $showValue, currentValue: $currentValue)
.foregroundColor(self.style.accentColor).padding(self.legend != nil ? 0 : 12).offset(y:self.legend != nil ? 0 : -10)
if(self.legend != nil) {
Text(self.legend!)
diff --git a/midnightgreen.gif b/midnightgreen.gif
deleted file mode 100644
index 7fc8f746..00000000
Binary files a/midnightgreen.gif and /dev/null differ
diff --git a/showcase1.gif b/showcase1.gif
deleted file mode 100644
index 52bba15a..00000000
Binary files a/showcase1.gif and /dev/null differ
diff --git a/showcase2.gif b/showcase2.gif
deleted file mode 100644
index 81a85c64..00000000
Binary files a/showcase2.gif and /dev/null differ
diff --git a/showcase3.gif b/showcase3.gif
deleted file mode 100644
index 497fed2a..00000000
Binary files a/showcase3.gif and /dev/null differ
diff --git a/showcase4.png b/showcase4.png
deleted file mode 100644
index a58c861f..00000000
Binary files a/showcase4.png and /dev/null differ
diff --git a/showcase5.png b/showcase5.png
deleted file mode 100644
index 319cd116..00000000
Binary files a/showcase5.png and /dev/null differ
diff --git a/watchos1.png b/watchos1.png
deleted file mode 100644
index 65d42e5e..00000000
Binary files a/watchos1.png and /dev/null differ