Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27e7e0dd1d | |||
| 74140af7a7 | |||
| 7568c5d40c | |||
| 24cf9eacb8 | |||
| c89b1e1480 | |||
| 82e8f249cc | |||
| 22a38a1d40 | |||
| b5b7c62645 | |||
| 3447d5c9bb | |||
| 640ddeb4d2 | |||
| ebd09f438e | |||
| 0303c3c14d | |||
| 068ea84ddf | |||
| fa8e015794 | |||
| dd7a1fc9bd | |||
| fada162030 | |||
| a242bd3c94 | |||
| c12c773af0 | |||
| 75804f470a | |||
| 04989ad159 | |||
| 7365bc91ef | |||
| 257e5fca30 | |||
| 6a9546bb1f | |||
| b230ed0369 | |||
| a8b4101c52 | |||
| 2a1b55f79f | |||
| 47731bfeff |
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!--- Provide a general summary of the issue in the Title above -->
|
||||
|
||||
|
||||
|
||||
## Description
|
||||
<!--- Provide a more detailed introduction to the issue itself, and why you consider it to be a bug -->
|
||||
|
||||
## Expected Behavior
|
||||
<!--- Tell us what should happen -->
|
||||
|
||||
## Actual Behavior
|
||||
<!--- Tell us what happens instead -->
|
||||
|
||||
## Possible Fix
|
||||
<!--- Not obligatory, but suggest a fix or reason for the bug -->
|
||||
|
||||
## Steps to Reproduce
|
||||
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
||||
<!--- reproduce this bug. Include code to reproduce, if relevant -->
|
||||
|
||||
## Your Environment
|
||||
<!--- Include as many relevant details about the environment -->
|
||||
* Version of this package used:
|
||||
* Device/Simulator:
|
||||
* Operating System and version:
|
||||
* Link to your project:
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Ask for a new feature
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--- Provide a general summary of the issue in the Title above -->
|
||||
|
||||
## Detailed Description
|
||||
<!--- Provide a detailed description of the change or addition you are proposing -->
|
||||
|
||||
## Context
|
||||
<!--- Why is this change important to you? How would you use it? -->
|
||||
<!--- How can it benefit other users? -->
|
||||
|
||||
## Possible Implementation
|
||||
<!--- Not obligatory, but suggest an idea for implementing addition or change -->
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: v2 ticket
|
||||
about: Create tasks for the upcoming new version
|
||||
title: ''
|
||||
labels: v2
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
# v2 ticket
|
||||
|
||||
## Ticket description:
|
||||
@@ -0,0 +1,29 @@
|
||||
<!--- Provide a general summary of your changes in the Title above -->
|
||||
|
||||
## Description
|
||||
<!--- Describe your changes in detail -->
|
||||
|
||||
## Motivation and Context
|
||||
<!--- Why is this change required? What problem does it solve? -->
|
||||
<!--- If it fixes an open issue, please link to the issue here. -->
|
||||
|
||||
## How Has This Been Tested?
|
||||
<!--- Please describe in detail how you tested your changes. -->
|
||||
<!--- Include details of your testing environment, and the tests you ran to -->
|
||||
<!--- see how your change affects other areas of the code, etc. -->
|
||||
|
||||
## Screenshots (if appropriate):
|
||||
|
||||
## Types of changes
|
||||
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
|
||||
- [ ] 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:
|
||||
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
|
||||
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
|
||||
- [ ] My code follows the code style of this project.
|
||||
- [ ] My change requires a change to the documentation.
|
||||
- [ ] I have updated the documentation accordingly.
|
||||
@@ -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
|
||||
+1
-1
@@ -6,7 +6,7 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "SwiftUICharts",
|
||||
platforms: [
|
||||
.iOS(.v13),.watchOS(.v6)
|
||||
.iOS(.v13), .watchOS(.v6), .macOS(.v10_15)
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries produced by a package, and make them visible to other packages.
|
||||
|
||||
@@ -4,11 +4,20 @@ Swift package for displaying charts effortlessly.
|
||||
|
||||

|
||||
|
||||
### Note:
|
||||
**A version 2.0 is coming soon!!! 🎉🎉🎉**, so please hold off your PRs for a while. I'm writing a new code base with more sleek code architecture with an option for easier expansion. I'll make beta releases so you can test betas.
|
||||
**If you'd like to contribute you can find tickets for the new version in the Issues under the `v2` tag, please read more at: [https://github.com/AppPear/ChartView/pull/89](https://github.com/AppPear/ChartView/pull/89)**
|
||||
|
||||
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-el5pnmba-FvyraEsI~EwxqWHNfHZWZg)
|
||||
|
||||
### Installation:
|
||||
|
||||
It requires iOS 13 and Xcode 11!
|
||||
@@ -27,7 +36,28 @@ Added an example project, with **iOS, watchOS** target: https://github.com/AppPe
|
||||
|
||||
## Line charts
|
||||
|
||||
**New full screen view called LineView!!!**
|
||||
**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!!!**
|
||||
|
||||

|
||||
|
||||
@@ -39,6 +69,14 @@ 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**
|
||||
|
||||
You can add a line chart with the following code:
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
@@ -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
|
||||
|
||||
@@ -11,16 +11,26 @@ import SwiftUI
|
||||
public struct BarChartRow : View {
|
||||
var data: [Double]
|
||||
var accentColor: Color
|
||||
var secondGradientAccentColor: Color?
|
||||
var gradient: GradientColor?
|
||||
|
||||
var maxValue: Double {
|
||||
data.max() ?? 0
|
||||
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..<self.data.count) { i in
|
||||
BarChartCell(value: self.normalizedValue(index: i), index: i, width: Float(geometry.frame(in: .local).width - 22), numberOfDataPoints: self.data.count, accentColor: self.accentColor, secondGradientAccentColor: self.secondGradientAccentColor, touchLocation: self.$touchLocation)
|
||||
ForEach(0..<self.data.count, id: \.self) { i in
|
||||
BarChartCell(value: self.normalizedValue(index: i),
|
||||
index: i,
|
||||
width: Float(geometry.frame(in: .local).width - 22),
|
||||
numberOfDataPoints: self.data.count,
|
||||
accentColor: self.accentColor,
|
||||
gradient: self.gradient,
|
||||
touchLocation: self.$touchLocation)
|
||||
.scaleEffect(self.touchLocation > 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())
|
||||
|
||||
@@ -38,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
|
||||
|
||||
@@ -19,7 +19,7 @@ public struct BarChartView : View {
|
||||
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
|
||||
@@ -50,7 +50,7 @@ public struct BarChartView : View {
|
||||
Rectangle()
|
||||
.fill(self.colorScheme == .dark ? self.darkModeStyle.backgroundColor : self.style.backgroundColor)
|
||||
.cornerRadius(20)
|
||||
.shadow(color: Color.gray, radius: self.dropShadow ? 8 : 0)
|
||||
.shadow(color: self.style.dropShadowColor, radius: self.dropShadow ? 8 : 0)
|
||||
VStack(alignment: .leading){
|
||||
HStack{
|
||||
if(!showValue){
|
||||
@@ -76,7 +76,7 @@ public struct BarChartView : View {
|
||||
}.padding()
|
||||
BarChartRow(data: data.points.map{$0.1},
|
||||
accentColor: self.colorScheme == .dark ? self.darkModeStyle.accentColor : self.style.accentColor,
|
||||
secondGradientAccentColor: self.colorScheme == .dark ? self.darkModeStyle.secondGradientColor : self.style.secondGradientColor,
|
||||
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!)
|
||||
@@ -85,7 +85,9 @@ public struct BarChartView : View {
|
||||
.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)
|
||||
title: .constant(self.getCurrentValue()!.0))
|
||||
.offset(x: self.getLabelViewOffset(touchLocation: self.touchLocation), y: -6)
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ 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")
|
||||
@@ -30,69 +30,104 @@ 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.OrangeStart,
|
||||
secondGradientColor: Colors.OrangeEnd,
|
||||
accentColor: Colors.OrangeEnd,
|
||||
secondGradientColor: Colors.OrangeStart,
|
||||
textColor: Color.black,
|
||||
legendTextColor: Color.gray)
|
||||
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)
|
||||
legendTextColor: Color.white,
|
||||
dropShadowColor: Color.gray)
|
||||
}
|
||||
|
||||
public struct ChartForm {
|
||||
@@ -100,45 +135,58 @@ public struct ChartForm {
|
||||
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 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 {
|
||||
public class ChartData: ObservableObject, Identifiable {
|
||||
@Published var points: [(String,Double)]
|
||||
var valuesGiven: Bool = false
|
||||
var ID = UUID()
|
||||
|
||||
public init<N: BinaryFloatingPoint>(points:[N]) {
|
||||
self.points = points.map{("", Double($0))}
|
||||
@@ -165,6 +213,24 @@ public class ChartData: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
public class MultiLineChartData: ChartData {
|
||||
var gradient: GradientColor
|
||||
|
||||
public init<N: BinaryFloatingPoint>(points:[N], gradient: GradientColor) {
|
||||
self.gradient = gradient
|
||||
super.init(points: points)
|
||||
}
|
||||
|
||||
public init<N: BinaryFloatingPoint>(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),
|
||||
|
||||
@@ -27,7 +27,7 @@ struct Legend: View {
|
||||
if (min < 0){
|
||||
return (frame.size.height-padding) / CGFloat(max - min)
|
||||
}else{
|
||||
return (frame.size.height-padding) / CGFloat(max + min)
|
||||
return (frame.size.height-padding) / CGFloat(max - min)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
|
||||
@@ -8,14 +8,19 @@
|
||||
|
||||
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
|
||||
@@ -23,26 +28,38 @@ struct Line: View {
|
||||
return frame.size.width / CGFloat(data.points.count-1)
|
||||
}
|
||||
var stepHeight: CGFloat {
|
||||
var min: Double?
|
||||
var max: Double?
|
||||
let points = self.data.onlyPoints()
|
||||
if let min = points.min(), let max = points.max(), min != max {
|
||||
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(points.max()! - points.min()!)
|
||||
return (frame.size.height-padding) / CGFloat(max - min)
|
||||
}else{
|
||||
return (frame.size.height-padding) / CGFloat(points.max()! + points.min()!)
|
||||
return (frame.size.height-padding) / CGFloat(max - min)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
var path: Path {
|
||||
let points = self.data.onlyPoints()
|
||||
return Path.quadCurvedPathWithPoints(points: points, step: CGPoint(x: stepWidth, y: stepHeight))
|
||||
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 Path.quadClosedCurvedPathWithPoints(points: points, step: CGPoint(x: stepWidth, y: stepHeight))
|
||||
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
|
||||
@@ -54,16 +71,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))
|
||||
.animation(Animation.easeOut(duration: 1.2).delay(Double(self.index)*0.4))
|
||||
.onAppear {
|
||||
self.showFull = true
|
||||
}
|
||||
.onDisappear {
|
||||
self.showFull = false
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
self.showFull = false
|
||||
}
|
||||
.drawingGroup()
|
||||
if(self.showIndicator) {
|
||||
IndicatorPoint()
|
||||
@@ -81,81 +98,10 @@ struct Line: View {
|
||||
|
||||
}
|
||||
|
||||
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:[Double], step:CGPoint) -> Path {
|
||||
var path = Path()
|
||||
if (points.count < 2){
|
||||
return path
|
||||
}
|
||||
guard var offset = points.min() else { return path }
|
||||
// offset -= 3
|
||||
var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
|
||||
path.move(to: p1)
|
||||
for pointIndex in 1..<points.count {
|
||||
let p2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
|
||||
let midPoint = CGPoint.midPointForPoints(p1: p1, p2: p2)
|
||||
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(p1: midPoint, p2: p1))
|
||||
path.addQuadCurve(to: p2, control: CGPoint.controlPointForPoints(p1: midPoint, p2: p2))
|
||||
p1 = p2
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
static func quadClosedCurvedPathWithPoints(points:[Double], step:CGPoint) -> Path {
|
||||
var path = Path()
|
||||
if (points.count < 2){
|
||||
return path
|
||||
}
|
||||
guard var offset = points.min() else { return path }
|
||||
// offset -= 3
|
||||
path.move(to: .zero)
|
||||
var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
|
||||
path.addLine(to: p1)
|
||||
for pointIndex in 1..<points.count {
|
||||
let p2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
|
||||
let midPoint = CGPoint.midPointForPoints(p1: p1, p2: p2)
|
||||
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(p1: midPoint, p2: p1))
|
||||
path.addQuadCurve(to: p2, control: CGPoint.controlPointForPoints(p1: midPoint, p2: p2))
|
||||
p1 = p2
|
||||
}
|
||||
path.addLine(to: CGPoint(x: p1.x, y: 0))
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,11 @@ public struct LineChartView: View {
|
||||
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 {
|
||||
@@ -30,8 +30,8 @@ public struct LineChartView: View {
|
||||
|
||||
}
|
||||
}
|
||||
let frame = CGSize(width: 180, height: 120)
|
||||
private var rateValue: Int
|
||||
var frame = CGSize(width: 180, height: 120)
|
||||
private var rateValue: Int?
|
||||
|
||||
public init(data: [Double],
|
||||
title: String,
|
||||
@@ -48,9 +48,10 @@ public struct LineChartView: View {
|
||||
self.style = style
|
||||
self.darkModeStyle = style.darkModeStyle != nil ? style.darkModeStyle! : Styles.lineViewDarkMode
|
||||
self.formSize = form!
|
||||
self.rateValue = rateValue!
|
||||
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 {
|
||||
@@ -58,7 +59,7 @@ public struct LineChartView: View {
|
||||
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)
|
||||
.shadow(color: self.style.dropShadowColor, radius: self.dropShadow ? 8 : 0)
|
||||
VStack(alignment: .leading){
|
||||
if(!self.showIndicatorDot){
|
||||
VStack(alignment: .leading, spacing: 8){
|
||||
@@ -72,12 +73,16 @@ public struct LineChartView: View {
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor :self.style.legendTextColor)
|
||||
}
|
||||
HStack {
|
||||
if (self.rateValue >= 0){
|
||||
Image(systemName: "arrow.up")
|
||||
}else{
|
||||
Image(systemName: "arrow.down")
|
||||
|
||||
if (self.rateValue ?? 0 != 0)
|
||||
{
|
||||
if (self.rateValue ?? 0 >= 0){
|
||||
Image(systemName: "arrow.up")
|
||||
}else{
|
||||
Image(systemName: "arrow.down")
|
||||
}
|
||||
Text("\(self.rateValue!)%")
|
||||
}
|
||||
Text("\(self.rateValue)%")
|
||||
}
|
||||
}
|
||||
.transition(.opacity)
|
||||
@@ -97,7 +102,11 @@ public struct LineChartView: View {
|
||||
GeometryReader{ geometry in
|
||||
Line(data: self.data,
|
||||
frame: .constant(geometry.frame(in: .local)),
|
||||
touchLocation: self.$touchLocation, showIndicator: self.$showIndicatorDot)
|
||||
touchLocation: self.$touchLocation,
|
||||
showIndicator: self.$showIndicatorDot,
|
||||
minDataValue: .constant(nil),
|
||||
maxDataValue: .constant(nil)
|
||||
)
|
||||
}
|
||||
.frame(width: frame.width, height: frame.height + 30)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 20))
|
||||
@@ -135,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ public struct LineView: View {
|
||||
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
|
||||
@@ -39,7 +39,7 @@ public struct LineView: View {
|
||||
self.darkModeStyle = style.darkModeStyle != nil ? style.darkModeStyle! : Styles.lineViewDarkMode
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
public var body: some View {
|
||||
GeometryReader{ geometry in
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Group{
|
||||
@@ -66,11 +66,20 @@ public struct LineView: View {
|
||||
}
|
||||
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 ,showBackground: false)
|
||||
.offset(x: 30, y: 0)
|
||||
.onAppear(){
|
||||
self.showLegend.toggle()
|
||||
}
|
||||
touchLocation: self.$indicatorLocation,
|
||||
showIndicator: self.$hideHorizontalLines,
|
||||
minDataValue: .constant(nil),
|
||||
maxDataValue: .constant(nil),
|
||||
showBackground: false,
|
||||
gradient: self.style.gradientColor
|
||||
)
|
||||
.offset(x: 30, y: -20)
|
||||
.onAppear(){
|
||||
self.showLegend = true
|
||||
}
|
||||
.onDisappear(){
|
||||
self.showLegend = false
|
||||
}
|
||||
}
|
||||
.frame(width: geometry.frame(in: .local).size.width, height: 240)
|
||||
.offset(x: 0, y: 40 )
|
||||
@@ -80,13 +89,13 @@ public struct LineView: View {
|
||||
}
|
||||
.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
|
||||
})
|
||||
.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
|
||||
@@ -100,7 +109,7 @@ public struct LineView: View {
|
||||
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]
|
||||
@@ -112,42 +121,12 @@ public struct LineView: View {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
struct IndicatorCircle: View {
|
||||
var body: some View {
|
||||
Circle()
|
||||
.size(width: 12, height: 12)
|
||||
.fill(Colors.BorderBlue)
|
||||
}
|
||||
}
|
||||
|
||||
struct MagnifierRect: View {
|
||||
@Binding var currentNumber: Double
|
||||
var valueSpecifier:String
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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? = 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!
|
||||
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 (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..<self.data.count) { i in
|
||||
Line(data: self.data[i],
|
||||
frame: .constant(geometry.frame(in: .local)),
|
||||
touchLocation: self.$touchLocation,
|
||||
showIndicator: self.$showIndicatorDot,
|
||||
minDataValue: .constant(self.globalMin),
|
||||
maxDataValue: .constant(self.globalMax),
|
||||
showBackground: false,
|
||||
gradient: self.data[i].getGradient(),
|
||||
index: i)
|
||||
}
|
||||
}
|
||||
}
|
||||
.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 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,31 +12,31 @@ extension 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):
|
||||
@@ -63,13 +63,13 @@ extension Path {
|
||||
}
|
||||
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
|
||||
@@ -114,6 +114,81 @@ extension Path {
|
||||
}
|
||||
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..<points.count {
|
||||
let p2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
|
||||
let midPoint = CGPoint.midPointForPoints(p1: p1, p2: p2)
|
||||
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(p1: midPoint, p2: p1))
|
||||
path.addQuadCurve(to: p2, control: CGPoint.controlPointForPoints(p1: midPoint, p2: p2))
|
||||
p1 = p2
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
static func quadClosedCurvedPathWithPoints(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 }
|
||||
path.move(to: .zero)
|
||||
var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
|
||||
path.addLine(to: p1)
|
||||
for pointIndex in 1..<points.count {
|
||||
let p2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
|
||||
let midPoint = CGPoint.midPointForPoints(p1: p1, p2: p2)
|
||||
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(p1: midPoint, p2: p1))
|
||||
path.addQuadCurve(to: p2, control: CGPoint.controlPointForPoints(p1: midPoint, p2: p2))
|
||||
p1 = p2
|
||||
}
|
||||
path.addLine(to: CGPoint(x: p1.x, y: 0))
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
|
||||
static func linePathWithPoints(points:[Double], step:CGPoint) -> 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..<points.count {
|
||||
let p2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
|
||||
path.addLine(to: p2)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
static func closedLinePathWithPoints(points:[Double], step:CGPoint) -> 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..<points.count {
|
||||
p1 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
|
||||
path.addLine(to: p1)
|
||||
}
|
||||
path.addLine(to: CGPoint(x: p1.x, y: 0))
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension CGPoint {
|
||||
@@ -122,40 +197,40 @@ extension CGPoint {
|
||||
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..<Int(steps) {
|
||||
let t0 = CGFloat(i) / steps
|
||||
let t1 = CGFloat(i+1) / steps
|
||||
let a = point(to: to, t: t0, control: control)
|
||||
let b = point(to: to, t: t1, control: control)
|
||||
|
||||
|
||||
dist += a.line(to: b)
|
||||
}
|
||||
return dist
|
||||
}
|
||||
|
||||
|
||||
func quadCurve(to: CGPoint, control: CGPoint, x: CGFloat) -> CGFloat {
|
||||
var dist: CGFloat = 0
|
||||
let steps: CGFloat = 100
|
||||
|
||||
|
||||
for i in 0..<Int(steps) {
|
||||
let t0 = CGFloat(i) / steps
|
||||
let t1 = CGFloat(i+1) / steps
|
||||
let a = point(to: to, t: t0, control: control)
|
||||
let b = point(to: to, t: t1, control: control)
|
||||
|
||||
|
||||
if a.x >= x {
|
||||
return dist
|
||||
} else if b.x > x {
|
||||
@@ -165,47 +240,47 @@ extension CGPoint {
|
||||
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..<Int(steps) {
|
||||
let t0 = CGFloat(i) / steps
|
||||
let t1 = CGFloat(i+1) / steps
|
||||
|
||||
|
||||
let a = point(to: to, t: t0, control1: control1, control2: control2)
|
||||
let b = point(to: to, t: t1, control1: control1, control2: control2)
|
||||
|
||||
|
||||
dist += a.line(to: b)
|
||||
}
|
||||
|
||||
|
||||
return dist
|
||||
}
|
||||
|
||||
|
||||
func curve(to: CGPoint, control1: CGPoint, control2: CGPoint, x: CGFloat) -> CGFloat {
|
||||
var dist: CGFloat = 0
|
||||
let steps: CGFloat = 100
|
||||
|
||||
|
||||
for i in 0..<Int(steps) {
|
||||
let t0 = CGFloat(i) / steps
|
||||
let t1 = CGFloat(i+1) / steps
|
||||
|
||||
|
||||
let a = point(to: to, t: t0, control1: control1, control2: control2)
|
||||
let b = point(to: to, t: t1, control1: control1, control2: control2)
|
||||
|
||||
|
||||
if a.x >= x {
|
||||
return dist
|
||||
} else if b.x > x {
|
||||
@@ -215,20 +290,20 @@ extension CGPoint {
|
||||
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
|
||||
@@ -237,7 +312,7 @@ extension CGPoint {
|
||||
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
|
||||
@@ -247,5 +322,32 @@ extension CGPoint {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,10 @@ public struct PieChartRow : View {
|
||||
#if DEBUG
|
||||
struct PieChartRow_Previews : PreviewProvider {
|
||||
static var previews: some View {
|
||||
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)).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)).frame(width: 100, height: 100)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,15 @@ public struct PieChartView : View {
|
||||
public var formSize:CGSize
|
||||
public var dropShadow: Bool
|
||||
|
||||
public init(data: [Double], title: String, legend: String? = nil, style: ChartStyle = Styles.pieChartStyleOne, form: CGSize? = ChartForm.medium, dropShadow: Bool? = true ){
|
||||
public init(data: [Double], title: String, legend: String? = nil, style: ChartStyle = Styles.pieChartStyleOne, form: CGSize? = ChartForm.medium, dropShadow: Bool? = true){
|
||||
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!
|
||||
}
|
||||
|
||||
@@ -30,7 +33,7 @@ public struct PieChartView : View {
|
||||
Rectangle()
|
||||
.fill(self.style.backgroundColor)
|
||||
.cornerRadius(20)
|
||||
.shadow(color: Color.gray, radius: self.dropShadow ? 12 : 0)
|
||||
.shadow(color: self.style.dropShadowColor, radius: self.dropShadow ? 12 : 0)
|
||||
VStack(alignment: .leading){
|
||||
HStack{
|
||||
Text(self.title)
|
||||
|
||||
Reference in New Issue
Block a user