├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── feature_request.md
│ └── v2Ticket.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ └── swift.yml
├── .gitignore
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcuserdata
│ │ ├── andrassamu.xcuserdatad
│ │ └── UserInterfaceState.xcuserstate
│ │ ├── roderic.xcuserdatad
│ │ └── UserInterfaceState.xcuserstate
│ │ └── samuandris.xcuserdatad
│ │ └── UserInterfaceState.xcuserstate
│ └── xcuserdata
│ ├── andrassamu.xcuserdatad
│ └── xcschemes
│ │ └── xcschememanagement.plist
│ └── samuandris.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── SwiftUICharts
│ ├── BarChart
│ ├── BarChartCell.swift
│ ├── BarChartRow.swift
│ ├── BarChartView.swift
│ └── LabelView.swift
│ ├── Helpers.swift
│ ├── LineChart
│ ├── IndicatorPoint.swift
│ ├── Legend.swift
│ ├── Line.swift
│ ├── LineChartView.swift
│ ├── LineView.swift
│ ├── MagnifierRect.swift
│ ├── MultiLineChartView.swift
│ └── Path+QuadCurve.swift
│ └── PieChart
│ ├── PieChartCell.swift
│ ├── PieChartHelpers.swift
│ ├── PieChartRow.swift
│ └── PieChartView.swift
└── Tests
├── LinuxMain.swift
└── SwiftUIChartsTests
├── SwiftUIChartsTests.swift
└── XCTestManifests.swift
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
12 |
13 | ## Description
14 |
15 |
16 | ## Expected Behavior
17 |
18 |
19 | ## Actual Behavior
20 |
21 |
22 | ## Possible Fix
23 |
24 |
25 | ## Steps to Reproduce
26 |
27 |
28 |
29 | ## Your Environment
30 |
31 | * Version of this package used:
32 | * Device/Simulator:
33 | * Operating System and version:
34 | * Link to your project:
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Ask for a new feature
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
12 | ## Detailed Description
13 |
14 |
15 | ## Context
16 |
17 |
18 |
19 | ## Possible Implementation
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/v2Ticket.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: v2 ticket
3 | about: Create tasks for the upcoming new version
4 | title: ''
5 | labels: v2
6 | assignees: ''
7 |
8 | ---
9 | # v2 ticket
10 |
11 | ## Ticket description:
12 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Description
4 |
5 |
6 | ## Motivation and Context
7 |
8 |
9 |
10 | ## How Has This Been Tested?
11 |
12 |
13 |
14 |
15 | ## Screenshots (if appropriate):
16 |
17 | ## Types of changes
18 |
19 | - [ ] Bug fix (non-breaking change which fixes an issue)
20 | - [ ] New feature (non-breaking change which adds functionality)
21 | - [ ] Breaking change (fix or feature that would cause existing functionality to change)
22 | - [ ] Non-functional change (Updating Documentation, CI automation, etc..)
23 |
24 | ## Checklist:
25 |
26 |
27 | - [ ] My code follows the code style of this project.
28 | - [ ] My change requires a change to the documentation.
29 | - [ ] I have updated the documentation accordingly.
30 |
--------------------------------------------------------------------------------
/.github/workflows/swift.yml:
--------------------------------------------------------------------------------
1 | name: Swift
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - new-version
8 | pull_request:
9 | branches:
10 | - master
11 | - new-version
12 | jobs:
13 | build:
14 |
15 | runs-on: macos-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Build
20 | run: swift build -v
21 | - name: Run tests
22 | run: swift test -v
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | .swiftpm
6 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcuserdata/andrassamu.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AppPear/ChartView/102b51bff6d2edcb4d0962d05795938199fe3eba/.swiftpm/xcode/package.xcworkspace/xcuserdata/andrassamu.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcuserdata/roderic.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AppPear/ChartView/102b51bff6d2edcb4d0962d05795938199fe3eba/.swiftpm/xcode/package.xcworkspace/xcuserdata/roderic.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcuserdata/samuandris.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AppPear/ChartView/102b51bff6d2edcb4d0962d05795938199fe3eba/.swiftpm/xcode/package.xcworkspace/xcuserdata/samuandris.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcuserdata/andrassamu.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | SwiftUICharts.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 | SuppressBuildableAutocreation
14 |
15 | SwiftUICharts
16 |
17 | primary
18 |
19 |
20 | SwiftUIChartsTests
21 |
22 | primary
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | SwiftUICharts.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 3
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Andras Samu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "SwiftUICharts",
8 | platforms: [
9 | .iOS(.v13), .watchOS(.v6), .macOS(.v11)
10 | ],
11 | products: [
12 | // Products define the executables and libraries produced by a package, and make them visible to other packages.
13 | .library(
14 | name: "SwiftUICharts",
15 | targets: ["SwiftUICharts"]),
16 | ],
17 | dependencies: [
18 | // Dependencies declare other packages that this package depends on.
19 | // .package(url: /* package url */, from: "1.0.0"),
20 | ],
21 | targets: [
22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
23 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
24 | .target(
25 | name: "SwiftUICharts",
26 | dependencies: []),
27 | .testTarget(
28 | name: "SwiftUIChartsTests",
29 | dependencies: ["SwiftUICharts"]),
30 | ]
31 | )
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftUICharts
2 |
3 | Swift package for displaying charts effortlessly.
4 |
5 | ## V2 Beta is here 🎉🎉🎉
6 |
7 | 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.
8 | So you decide, whether you build a fully fledged interactive view, or just display a bare bone chart
9 |
10 | * [How to install SwiftUI ChartView](https://github.com/AppPear/ChartView/wiki/How-to-install-SwiftUI-ChartView)
11 |
12 | * [How to create your first chart](https://github.com/AppPear/ChartView/wiki/How-to-create-your-first-chart)
13 |
14 | ### It supports interactions and animations
15 |
16 |
17 | ### It is fully customizable, and works together with native SwiftUI elements well
18 |
19 |
20 |
21 | ## Original (stable) version:
22 |
23 |
24 |
25 | ### Usage
26 |
27 | It supports:
28 | * Line charts
29 | * Bar charts
30 | * Pie charts
31 |
32 | ### Slack
33 | Join our Slack channel for day to day conversation and more insights:
34 |
35 | [Slack invite link](https://join.slack.com/t/swiftuichartview/shared_invite/zt-g6mxioq8-j3iUTF1YKX7D23ML3qcc4g)
36 |
37 | ### Installation:
38 |
39 | It requires iOS 13 and Xcode 11!
40 |
41 | In Xcode go to `File -> Swift Packages -> Add Package Dependency` and paste in the repo's url: `https://github.com/AppPear/ChartView`
42 |
43 | ### Usage:
44 |
45 | import the package in the file you would like to use it: `import SwiftUICharts`
46 |
47 | You can display a Chart by adding a chart view to your parent view:
48 |
49 | ### Demo
50 |
51 | Added an example project, with **iOS, watchOS** target: https://github.com/AppPear/ChartViewDemo
52 |
53 | ## Line charts
54 |
55 | **LineChartView with multiple lines!**
56 | 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.
57 |
58 |
59 |
60 | Usage:
61 | ```swift
62 | 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")
63 | ```
64 | Gradient colors are now under the `GradientColor` struct you can create your own gradient by `GradientColor(start: Color, end: Color)`
65 |
66 | Available preset gradients:
67 | * orange
68 | * blue
69 | * green
70 | * blu
71 | * bluPurpl
72 | * purple
73 | * prplPink
74 | * prplNeon
75 | * orngPink
76 |
77 | **Full screen view called LineView!!!**
78 |
79 |
80 | ```swift
81 | LineView(data: [8,23,54,32,12,37,7,23,43], title: "Line chart", legend: "Full screen") // legend is optional, use optional .padding()
82 | ```
83 |
84 | Adopts to dark mode automatically
85 |
86 |
87 |
88 | You can add your custom darkmode style by specifying:
89 |
90 | ```swift
91 | let myCustomStyle = ChartStyle(...)
92 | let myCutsomDarkModeStyle = ChartStyle(...)
93 | myCustomStyle.darkModeStyle = myCutsomDarkModeStyle
94 | ```
95 |
96 | **Line chart is interactive, so you can drag across to reveal the data points**
97 |
98 | You can add a line chart with the following code:
99 |
100 | ```swift
101 | LineChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", legend: "Legendary") // legend is optional
102 | ```
103 |
104 | **Turn drop shadow off by adding to the Initialiser: `dropShadow: false`**
105 |
106 |
107 | ## Bar charts
108 |
109 |
110 | **[New feature] you can display labels also along values and points for each bar to descirbe your data better!**
111 | **Bar chart is interactive, so you can drag across to reveal the data points**
112 |
113 | You can add a bar chart with the following code:
114 |
115 | Labels and points:
116 |
117 | ```swift
118 | 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
119 | ```
120 | Only points:
121 |
122 | ```swift
123 | BarChartView(data: ChartData(points: [8,23,54,32,12,37,7,23,43]), title: "Title", legend: "Legendary") // legend is optional
124 | ```
125 |
126 | **ChartData** structure
127 | Stores values in data pairs (actually tuple): `(String,Double)`
128 | * you can have duplicate values
129 | * keeps the data order
130 |
131 | You can initialise ChartData multiple ways:
132 | * For integer values: `ChartData(points: [8,23,54,32,12,37,7,23,43])`
133 | * For floating point values: `ChartData(points: [2.34,3.14,4.56])`
134 | * For label,value pairs: `ChartData(values: [("2018 Q4",63150), ("2019 Q1",50900)])`
135 |
136 |
137 | You can add different formats:
138 | * Small `ChartForm.small`
139 | * Medium `ChartForm.medium`
140 | * Large `ChartForm.large`
141 |
142 | ```swift
143 | BarChartView(data: ChartData(points: [8,23,54,32,12,37,7,23,43]), title: "Title", form: ChartForm.small)
144 | ```
145 |
146 | For floating point numbers, you can set a custom specifier:
147 |
148 | ```swift
149 | BarChartView(data: ChartData(points:[1.23,2.43,3.37]) ,title: "A", valueSpecifier: "%.2f")
150 | ```
151 | For integers you can disable by passing: `valueSpecifier: "%.0f"`
152 |
153 |
154 | You can set your custom image in the upper right corner by passing in the initialiser: `cornerImage:Image(systemName: "waveform.path.ecg")`
155 |
156 |
157 | **Turn drop shadow off by adding to the Initialiser: `dropShadow: false`**
158 |
159 | ### You can customize styling of the chart with a ChartStyle object:
160 |
161 | Customizable:
162 | * background color
163 | * accent color
164 | * second gradient color
165 | * text color
166 | * legend text color
167 |
168 | ```swift
169 | let chartStyle = ChartStyle(backgroundColor: Color.black, accentColor: Colors.OrangeStart, secondGradientColor: Colors.OrangeEnd, chartFormSize: ChartForm.medium, textColor: Color.white, legendTextColor: Color.white )
170 | ...
171 | BarChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", style: chartStyle)
172 | ```
173 |
174 | You can access built-in styles:
175 | ```swift
176 | BarChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", style: Styles.barChartMidnightGreen)
177 | ```
178 | #### All styles available as a preset:
179 | * barChartStyleOrangeLight
180 | * barChartStyleOrangeDark
181 | * barChartStyleNeonBlueLight
182 | * barChartStyleNeonBlueDark
183 | * barChartMidnightGreenLight
184 | * barChartMidnightGreenDark
185 |
186 |
187 |
188 |
189 |
190 | ### You can customize the size of the chart with a ChartForm object:
191 |
192 | **ChartForm**
193 | * `.small`
194 | * `.medium`
195 | * `.large`
196 | * `.detail`
197 |
198 | ```swift
199 | BarChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", form: ChartForm.small)
200 | ```
201 |
202 | ### You can choose whether bar is animated or not after completing your gesture.
203 |
204 | If you want to animate back movement after completing your gesture, you set `animatedToBack` as `true`.
205 |
206 | ### WatchOS support for Bar charts:
207 |
208 |
209 |
210 | ## Pie charts
211 |
212 |
213 | You can add a pie chart with the following code:
214 |
215 | ```swift
216 | PieChartView(data: [8,23,54,32], title: "Title", legend: "Legendary") // legend is optional
217 | ```
218 |
219 | **Turn drop shadow off by adding to the Initialiser: `dropShadow: false`**
220 |
221 |
--------------------------------------------------------------------------------
/Sources/SwiftUICharts/BarChart/BarChartCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChartCell.swift
3 | // ChartView
4 | //
5 | // Created by András Samu on 2019. 06. 12..
6 | // Copyright © 2019. András Samu. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public struct BarChartCell : View {
12 | var value: Double
13 | var index: Int = 0
14 | var width: Float
15 | var numberOfDataPoints: Int
16 | var cellWidth: Double {
17 | return Double(width)/(Double(numberOfDataPoints) * 1.5)
18 | }
19 | var accentColor: Color
20 | var gradient: GradientColor?
21 |
22 | @State var scaleValue: Double = 0
23 | @Binding var touchLocation: CGFloat
24 | public var body: some View {
25 | ZStack {
26 | RoundedRectangle(cornerRadius: 4)
27 | .fill(LinearGradient(gradient: gradient?.getGradient() ?? GradientColor(start: accentColor, end: accentColor).getGradient(), startPoint: .bottom, endPoint: .top))
28 | }
29 | .frame(width: CGFloat(self.cellWidth))
30 | .scaleEffect(CGSize(width: 1, height: self.scaleValue), anchor: .bottom)
31 | .onAppear(){
32 | self.scaleValue = self.value
33 | }
34 | .animation(Animation.spring().delay(self.touchLocation < 0 ? Double(self.index) * 0.04 : 0))
35 | }
36 | }
37 |
38 | #if DEBUG
39 | struct ChartCell_Previews : PreviewProvider {
40 | static var previews: some View {
41 | BarChartCell(value: Double(0.75), width: 320, numberOfDataPoints: 12, accentColor: Colors.OrangeStart, gradient: nil, touchLocation: .constant(-1))
42 | }
43 | }
44 | #endif
45 |
--------------------------------------------------------------------------------
/Sources/SwiftUICharts/BarChart/BarChartRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChartRow.swift
3 | // ChartView
4 | //
5 | // Created by András Samu on 2019. 06. 12..
6 | // Copyright © 2019. András Samu. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public struct BarChartRow : View {
12 | var data: [Double]
13 | var accentColor: Color
14 | var gradient: GradientColor?
15 |
16 | var maxValue: Double {
17 | guard let max = data.max() else {
18 | return 1
19 | }
20 | return max != 0 ? max : 1
21 | }
22 | @Binding var touchLocation: CGFloat
23 | public var body: some View {
24 | GeometryReader { geometry in
25 | HStack(alignment: .bottom, spacing: (geometry.frame(in: .local).width-22)/CGFloat(self.data.count * 3)){
26 | 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)
35 | .animation(.spring())
36 |
37 | }
38 | }
39 | .padding([.top, .leading, .trailing], 10)
40 | }
41 | }
42 |
43 | func normalizedValue(index: Int) -> Double {
44 | return Double(self.data[index])/Double(self.maxValue)
45 | }
46 | }
47 |
48 | #if DEBUG
49 | struct ChartRow_Previews : PreviewProvider {
50 | static var previews: some View {
51 | Group {
52 | BarChartRow(data: [0], accentColor: Colors.OrangeStart, touchLocation: .constant(-1))
53 | BarChartRow(data: [8,23,54,32,12,37,7], accentColor: Colors.OrangeStart, touchLocation: .constant(-1))
54 | }
55 | }
56 | }
57 | #endif
58 |
--------------------------------------------------------------------------------
/Sources/SwiftUICharts/BarChart/BarChartView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChartView.swift
3 | // ChartView
4 | //
5 | // Created by András Samu on 2019. 06. 12..
6 | // Copyright © 2019. András Samu. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public struct BarChartView : View {
12 | @Environment(\.colorScheme) var colorScheme: ColorScheme
13 | private var data: ChartData
14 | public var title: String
15 | public var legend: String?
16 | public var style: ChartStyle
17 | public var darkModeStyle: ChartStyle
18 | public var formSize:CGSize
19 | public var dropShadow: Bool
20 | public var cornerImage: Image?
21 | public var valueSpecifier:String
22 | public var animatedToBack: Bool
23 |
24 | @State private var touchLocation: CGFloat = -1.0
25 | @State private var showValue: Bool = false
26 | @State private var showLabelValue: Bool = false
27 | @State private var currentValue: Double = 0 {
28 | didSet{
29 | if(oldValue != self.currentValue && self.showValue) {
30 | HapticFeedback.playSelection()
31 | }
32 | }
33 | }
34 | var isFullWidth:Bool {
35 | return self.formSize == ChartForm.large
36 | }
37 | 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){
38 | self.data = data
39 | self.title = title
40 | self.legend = legend
41 | self.style = style
42 | self.darkModeStyle = style.darkModeStyle != nil ? style.darkModeStyle! : Styles.barChartStyleOrangeDark
43 | self.formSize = form!
44 | self.dropShadow = dropShadow!
45 | self.cornerImage = cornerImage
46 | self.valueSpecifier = valueSpecifier!
47 | self.animatedToBack = animatedToBack
48 | }
49 |
50 | public var body: some View {
51 | ZStack{
52 | Rectangle()
53 | .fill(self.colorScheme == .dark ? self.darkModeStyle.backgroundColor : self.style.backgroundColor)
54 | .cornerRadius(20)
55 | .shadow(color: self.style.dropShadowColor, radius: self.dropShadow ? 8 : 0)
56 | VStack(alignment: .leading){
57 | HStack{
58 | if(!showValue){
59 | Text(self.title)
60 | .font(.headline)
61 | .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor)
62 | }else{
63 | Text("\(self.currentValue, specifier: self.valueSpecifier)")
64 | .font(.headline)
65 | .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor)
66 | }
67 | if(self.formSize == ChartForm.large && self.legend != nil && !showValue) {
68 | Text(self.legend!)
69 | .font(.callout)
70 | .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.accentColor : self.style.accentColor)
71 | .transition(.opacity)
72 | .animation(.easeOut)
73 | }
74 | Spacer()
75 | self.cornerImage
76 | .imageScale(.large)
77 | .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor)
78 | }.padding()
79 | BarChartRow(data: data.points.map{$0.1},
80 | accentColor: self.colorScheme == .dark ? self.darkModeStyle.accentColor : self.style.accentColor,
81 | gradient: self.colorScheme == .dark ? self.darkModeStyle.gradientColor : self.style.gradientColor,
82 | touchLocation: self.$touchLocation)
83 | if self.legend != nil && self.formSize == ChartForm.medium && !self.showLabelValue{
84 | Text(self.legend!)
85 | .font(.headline)
86 | .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor)
87 | .padding()
88 | }else if (self.data.valuesGiven && self.getCurrentValue() != nil) {
89 | LabelView(arrowOffset: self.getArrowOffset(touchLocation: self.touchLocation),
90 | title: .constant(self.getCurrentValue()!.0))
91 | .offset(x: self.getLabelViewOffset(touchLocation: self.touchLocation), y: -6)
92 | .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor)
93 | }
94 |
95 | }
96 | }.frame(minWidth:self.formSize.width,
97 | maxWidth: self.isFullWidth ? .infinity : self.formSize.width,
98 | minHeight:self.formSize.height,
99 | maxHeight:self.formSize.height)
100 | .gesture(DragGesture()
101 | .onChanged({ value in
102 | self.touchLocation = value.location.x/self.formSize.width
103 | self.showValue = true
104 | self.currentValue = self.getCurrentValue()?.1 ?? 0
105 | if(self.data.valuesGiven && self.formSize == ChartForm.medium) {
106 | self.showLabelValue = true
107 | }
108 | })
109 | .onEnded({ value in
110 | if animatedToBack {
111 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
112 | withAnimation(Animation.easeOut(duration: 1)) {
113 | self.showValue = false
114 | self.showLabelValue = false
115 | self.touchLocation = -1
116 | }
117 | }
118 | } else {
119 | self.showValue = false
120 | self.showLabelValue = false
121 | self.touchLocation = -1
122 | }
123 | })
124 | )
125 | .gesture(TapGesture()
126 | )
127 | }
128 |
129 | func getArrowOffset(touchLocation:CGFloat) -> Binding {
130 | let realLoc = (self.touchLocation * self.formSize.width) - 50
131 | if realLoc < 10 {
132 | return .constant(realLoc - 10)
133 | }else if realLoc > self.formSize.width-110 {
134 | return .constant((self.formSize.width-110 - realLoc) * -1)
135 | } else {
136 | return .constant(0)
137 | }
138 | }
139 |
140 | func getLabelViewOffset(touchLocation:CGFloat) -> CGFloat {
141 | return min(self.formSize.width-110,max(10,(self.touchLocation * self.formSize.width) - 50))
142 | }
143 |
144 | func getCurrentValue() -> (String,Double)? {
145 | guard self.data.points.count > 0 else { return nil}
146 | 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))))))
147 | return self.data.points[index]
148 | }
149 | }
150 |
151 | #if DEBUG
152 | struct ChartView_Previews : PreviewProvider {
153 | static var previews: some View {
154 | BarChartView(data: TestData.values ,
155 | title: "Model 3 sales",
156 | legend: "Quarterly",
157 | valueSpecifier: "%.0f")
158 | }
159 | }
160 | #endif
161 |
--------------------------------------------------------------------------------
/Sources/SwiftUICharts/BarChart/LabelView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LabelView.swift
3 | // BarChart
4 | //
5 | // Created by Samu András on 2020. 01. 08..
6 | // Copyright © 2020. Samu András. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct LabelView: View {
12 | @Binding var arrowOffset: CGFloat
13 | @Binding var title:String
14 | var body: some View {
15 | VStack{
16 | 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)
17 | ZStack{
18 | RoundedRectangle(cornerRadius: 8).frame(width: 100, height: 32, alignment: .center).foregroundColor(Color.white).shadow(radius: 8)
19 | Text(self.title).font(.caption).bold()
20 | ArrowUp().fill(Color.white).frame(width: 20, height: 12, alignment: .center).zIndex(999).offset(x: getArrowOffset(offset:self.arrowOffset), y: -20)
21 |
22 | }
23 | }
24 | }
25 |
26 | func getArrowOffset(offset: CGFloat) -> CGFloat {
27 | return max(-36,min(36, offset))
28 | }
29 | }
30 |
31 | struct ArrowUp: Shape {
32 | func path(in rect: CGRect) -> Path {
33 | var path = Path()
34 | path.move(to: CGPoint(x: 0, y: rect.height))
35 | path.addLine(to: CGPoint(x: rect.width/2, y: 0))
36 | path.addLine(to: CGPoint(x: rect.width, y: rect.height))
37 | path.closeSubpath()
38 | return path
39 | }
40 | }
41 |
42 | struct LabelView_Previews: PreviewProvider {
43 | static var previews: some View {
44 | LabelView(arrowOffset: .constant(0), title: .constant("Tesla model 3"))
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/SwiftUICharts/Helpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by András Samu on 2019. 07. 19..
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | public struct Colors {
12 | public static let color1:Color = Color(hexString: "#E2FAE7")
13 | public static let color1Accent:Color = Color(hexString: "#72BF82")
14 | public static let color2:Color = Color(hexString: "#EEF1FF")
15 | public static let color2Accent:Color = Color(hexString: "#4266E8")
16 | public static let color3:Color = Color(hexString: "#FCECEA")
17 | public static let color3Accent:Color = Color(hexString: "#E1614C")
18 | public static let OrangeEnd:Color = Color(hexString: "#FF782C")
19 | public static let OrangeStart:Color = Color(hexString: "#EC2301")
20 | public static let LegendText:Color = Color(hexString: "#A7A6A8")
21 | public static let LegendColor:Color = Color(hexString: "#E8E7EA")
22 | public static let LegendDarkColor:Color = Color(hexString: "#545454")
23 | public static let IndicatorKnob:Color = Color(hexString: "#FF57A6")
24 | public static let GradientUpperBlue:Color = Color(hexString: "#C2E8FF")
25 | public static let GradinetUpperBlue1:Color = Color(hexString: "#A8E1FF")
26 | public static let GradientPurple:Color = Color(hexString: "#7B75FF")
27 | public static let GradientNeonBlue:Color = Color(hexString: "#6FEAFF")
28 | public static let GradientLowerBlue:Color = Color(hexString: "#F1F9FF")
29 | public static let DarkPurple:Color = Color(hexString: "#1B205E")
30 | public static let BorderBlue:Color = Color(hexString: "#4EBCFF")
31 | }
32 |
33 | public struct GradientColor {
34 | public let start: Color
35 | public let end: Color
36 |
37 | public init(start: Color, end: Color) {
38 | self.start = start
39 | self.end = end
40 | }
41 |
42 | public func getGradient() -> Gradient {
43 | return Gradient(colors: [start, end])
44 | }
45 | }
46 |
47 | public struct GradientColors {
48 | public static let orange = GradientColor(start: Colors.OrangeStart, end: Colors.OrangeEnd)
49 | public static let blue = GradientColor(start: Colors.GradientPurple, end: Colors.GradientNeonBlue)
50 | public static let green = GradientColor(start: Color(hexString: "0BCDF7"), end: Color(hexString: "A2FEAE"))
51 | public static let blu = GradientColor(start: Color(hexString: "0591FF"), end: Color(hexString: "29D9FE"))
52 | public static let bluPurpl = GradientColor(start: Color(hexString: "4ABBFB"), end: Color(hexString: "8C00FF"))
53 | public static let purple = GradientColor(start: Color(hexString: "741DF4"), end: Color(hexString: "C501B0"))
54 | public static let prplPink = GradientColor(start: Color(hexString: "BC05AF"), end: Color(hexString: "FF1378"))
55 | public static let prplNeon = GradientColor(start: Color(hexString: "FE019A"), end: Color(hexString: "FE0BF4"))
56 | public static let orngPink = GradientColor(start: Color(hexString: "FF8E2D"), end: Color(hexString: "FF4E7A"))
57 | }
58 |
59 | public struct Styles {
60 | public static let lineChartStyleOne = ChartStyle(
61 | backgroundColor: Color.white,
62 | accentColor: Colors.OrangeStart,
63 | secondGradientColor: Colors.OrangeEnd,
64 | textColor: Color.black,
65 | legendTextColor: Color.gray,
66 | dropShadowColor: Color.gray)
67 |
68 | public static let barChartStyleOrangeLight = ChartStyle(
69 | backgroundColor: Color.white,
70 | accentColor: Colors.OrangeStart,
71 | secondGradientColor: Colors.OrangeEnd,
72 | textColor: Color.black,
73 | legendTextColor: Color.gray,
74 | dropShadowColor: Color.gray)
75 |
76 | public static let barChartStyleOrangeDark = ChartStyle(
77 | backgroundColor: Color.black,
78 | accentColor: Colors.OrangeStart,
79 | secondGradientColor: Colors.OrangeEnd,
80 | textColor: Color.white,
81 | legendTextColor: Color.gray,
82 | dropShadowColor: Color.gray)
83 |
84 | public static let barChartStyleNeonBlueLight = ChartStyle(
85 | backgroundColor: Color.white,
86 | accentColor: Colors.GradientNeonBlue,
87 | secondGradientColor: Colors.GradientPurple,
88 | textColor: Color.black,
89 | legendTextColor: Color.gray,
90 | dropShadowColor: Color.gray)
91 |
92 | public static let barChartStyleNeonBlueDark = ChartStyle(
93 | backgroundColor: Color.black,
94 | accentColor: Colors.GradientNeonBlue,
95 | secondGradientColor: Colors.GradientPurple,
96 | textColor: Color.white,
97 | legendTextColor: Color.gray,
98 | dropShadowColor: Color.gray)
99 |
100 | public static let barChartMidnightGreenDark = ChartStyle(
101 | backgroundColor: Color(hexString: "#36534D"), //3B5147, 313D34
102 | accentColor: Color(hexString: "#FFD603"),
103 | secondGradientColor: Color(hexString: "#FFCA04"),
104 | textColor: Color.white,
105 | legendTextColor: Color(hexString: "#D2E5E1"),
106 | dropShadowColor: Color.gray)
107 |
108 | public static let barChartMidnightGreenLight = ChartStyle(
109 | backgroundColor: Color.white,
110 | accentColor: Color(hexString: "#84A094"), //84A094 , 698378
111 | secondGradientColor: Color(hexString: "#50675D"),
112 | textColor: Color.black,
113 | legendTextColor:Color.gray,
114 | dropShadowColor: Color.gray)
115 |
116 | public static let pieChartStyleOne = ChartStyle(
117 | backgroundColor: Color.white,
118 | accentColor: Colors.OrangeEnd,
119 | secondGradientColor: Colors.OrangeStart,
120 | textColor: Color.black,
121 | legendTextColor: Color.gray,
122 | dropShadowColor: Color.gray)
123 |
124 | public static let lineViewDarkMode = ChartStyle(
125 | backgroundColor: Color.black,
126 | accentColor: Colors.OrangeStart,
127 | secondGradientColor: Colors.OrangeEnd,
128 | textColor: Color.white,
129 | legendTextColor: Color.white,
130 | dropShadowColor: Color.gray)
131 | }
132 |
133 | public struct ChartForm {
134 | #if os(watchOS)
135 | public static let small = CGSize(width:120, height:90)
136 | public static let medium = CGSize(width:120, height:160)
137 | public static let large = CGSize(width:180, height:90)
138 | public static let extraLarge = CGSize(width:180, height:90)
139 | public static let detail = CGSize(width:180, height:160)
140 | #else
141 | public static let small = CGSize(width:180, height:120)
142 | public static let medium = CGSize(width:180, height:240)
143 | public static let large = CGSize(width:360, height:120)
144 | public static let extraLarge = CGSize(width:360, height:240)
145 | public static let detail = CGSize(width:180, height:120)
146 | #endif
147 | }
148 |
149 | public class ChartStyle {
150 | public var backgroundColor: Color
151 | public var accentColor: Color
152 | public var gradientColor: GradientColor
153 | public var textColor: Color
154 | public var legendTextColor: Color
155 | public var dropShadowColor: Color
156 | public weak var darkModeStyle: ChartStyle?
157 |
158 | public init(backgroundColor: Color, accentColor: Color, secondGradientColor: Color, textColor: Color, legendTextColor: Color, dropShadowColor: Color){
159 | self.backgroundColor = backgroundColor
160 | self.accentColor = accentColor
161 | self.gradientColor = GradientColor(start: accentColor, end: secondGradientColor)
162 | self.textColor = textColor
163 | self.legendTextColor = legendTextColor
164 | self.dropShadowColor = dropShadowColor
165 | }
166 |
167 | public init(backgroundColor: Color, accentColor: Color, gradientColor: GradientColor, textColor: Color, legendTextColor: Color, dropShadowColor: Color){
168 | self.backgroundColor = backgroundColor
169 | self.accentColor = accentColor
170 | self.gradientColor = gradientColor
171 | self.textColor = textColor
172 | self.legendTextColor = legendTextColor
173 | self.dropShadowColor = dropShadowColor
174 | }
175 |
176 | public init(formSize: CGSize){
177 | self.backgroundColor = Color.white
178 | self.accentColor = Colors.OrangeStart
179 | self.gradientColor = GradientColors.orange
180 | self.legendTextColor = Color.gray
181 | self.textColor = Color.black
182 | self.dropShadowColor = Color.gray
183 | }
184 | }
185 |
186 | public class ChartData: ObservableObject, Identifiable {
187 | @Published var points: [(String,Double)]
188 | var valuesGiven: Bool = false
189 | var ID = UUID()
190 |
191 | public init(points:[N]) {
192 | self.points = points.map{("", Double($0))}
193 | }
194 | public init(values:[(String,N)]){
195 | self.points = values.map{($0.0, Double($0.1))}
196 | self.valuesGiven = true
197 | }
198 | public init(values:[(String,N)]){
199 | self.points = values.map{($0.0, Double($0.1))}
200 | self.valuesGiven = true
201 | }
202 | public init(numberValues:[(N,N)]){
203 | self.points = numberValues.map{(String($0.0), Double($0.1))}
204 | self.valuesGiven = true
205 | }
206 | public init(numberValues:[(N,N)]){
207 | self.points = numberValues.map{(String($0.0), Double($0.1))}
208 | self.valuesGiven = true
209 | }
210 |
211 | public func onlyPoints() -> [Double] {
212 | return self.points.map{ $0.1 }
213 | }
214 | }
215 |
216 | public class MultiLineChartData: ChartData {
217 | var gradient: GradientColor
218 |
219 | public init(points:[N], gradient: GradientColor) {
220 | self.gradient = gradient
221 | super.init(points: points)
222 | }
223 |
224 | public init(points:[N], color: Color) {
225 | self.gradient = GradientColor(start: color, end: color)
226 | super.init(points: points)
227 | }
228 |
229 | public func getGradient() -> GradientColor {
230 | return self.gradient
231 | }
232 | }
233 |
234 | public class TestData{
235 | static public var data:ChartData = ChartData(points: [37,72,51,22,39,47,66,85,50])
236 | static public var values:ChartData = ChartData(values: [("2017 Q3",220),
237 | ("2017 Q4",1550),
238 | ("2018 Q1",8180),
239 | ("2018 Q2",18440),
240 | ("2018 Q3",55840),
241 | ("2018 Q4",63150), ("2019 Q1",50900), ("2019 Q2",77550), ("2019 Q3",79600), ("2019 Q4",92550)])
242 |
243 | }
244 |
245 | extension Color {
246 | init(hexString: String) {
247 | let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
248 | var int = UInt64()
249 | Scanner(string: hex).scanHexInt64(&int)
250 | let r, g, b: UInt64
251 | switch hex.count {
252 | case 3: // RGB (12-bit)
253 | (r, g, b) = ((int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
254 | case 6: // RGB (24-bit)
255 | (r, g, b) = (int >> 16, int >> 8 & 0xFF, int & 0xFF)
256 | case 8: // ARGB (32-bit)
257 | (r, g, b) = (int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
258 | default:
259 | (r, g, b) = (0, 0, 0)
260 | }
261 | self.init(red: Double(r) / 255, green: Double(g) / 255, blue: Double(b) / 255)
262 | }
263 | }
264 |
265 | class HapticFeedback {
266 | #if os(watchOS)
267 | //watchOS implementation
268 | static func playSelection() -> Void {
269 | WKInterfaceDevice.current().play(.click)
270 | }
271 | #elseif os(iOS)
272 | //iOS implementation
273 | let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
274 | static func playSelection() -> Void {
275 | UISelectionFeedbackGenerator().selectionChanged()
276 | }
277 | #else
278 | static func playSelection() -> Void {
279 | //No-op
280 | }
281 | #endif
282 | }
283 |
--------------------------------------------------------------------------------
/Sources/SwiftUICharts/LineChart/IndicatorPoint.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IndicatorPoint.swift
3 | // LineChart
4 | //
5 | // Created by András Samu on 2019. 09. 03..
6 | // Copyright © 2019. András Samu. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct IndicatorPoint: View {
12 | var body: some View {
13 | ZStack{
14 | Circle()
15 | .fill(Colors.IndicatorKnob)
16 | Circle()
17 | .stroke(Color.white, style: StrokeStyle(lineWidth: 4))
18 | }
19 | .frame(width: 14, height: 14)
20 | .shadow(color: Colors.LegendColor, radius: 6, x: 0, y: 6)
21 | }
22 | }
23 |
24 | struct IndicatorPoint_Previews: PreviewProvider {
25 | static var previews: some View {
26 | IndicatorPoint()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/SwiftUICharts/LineChart/Legend.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Legend.swift
3 | // LineChart
4 | //
5 | // Created by András Samu on 2019. 09. 02..
6 | // Copyright © 2019. András Samu. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct Legend: View {
12 | @ObservedObject var data: ChartData
13 | @Binding var frame: CGRect
14 | @Binding var hideHorizontalLines: Bool
15 | @Environment(\.colorScheme) var colorScheme: ColorScheme
16 | var specifier: String = "%.2f"
17 | let padding:CGFloat = 3
18 |
19 | var stepWidth: CGFloat {
20 | if data.points.count < 2 {
21 | return 0
22 | }
23 | return frame.size.width / CGFloat(data.points.count-1)
24 | }
25 | var stepHeight: CGFloat {
26 | let points = self.data.onlyPoints()
27 | if let min = points.min(), let max = points.max(), min != max {
28 | if (min < 0){
29 | return (frame.size.height-padding) / CGFloat(max - min)
30 | }else{
31 | return (frame.size.height-padding) / CGFloat(max - min)
32 | }
33 | }
34 | return 0
35 | }
36 |
37 | var min: CGFloat {
38 | let points = self.data.onlyPoints()
39 | return CGFloat(points.min() ?? 0)
40 | }
41 |
42 | var body: some View {
43 | ZStack(alignment: .topLeading){
44 | ForEach((0...4), id: \.self) { height in
45 | HStack(alignment: .center){
46 | Text("\(self.getYLegendSafe(height: height), specifier: specifier)").offset(x: 0, y: self.getYposition(height: height) )
47 | .foregroundColor(Colors.LegendText)
48 | .font(.caption)
49 | self.line(atHeight: self.getYLegendSafe(height: height), width: self.frame.width)
50 | .stroke(self.colorScheme == .dark ? Colors.LegendDarkColor : Colors.LegendColor, style: StrokeStyle(lineWidth: 1.5, lineCap: .round, dash: [5,height == 0 ? 0 : 10]))
51 | .opacity((self.hideHorizontalLines && height != 0) ? 0 : 1)
52 | .rotationEffect(.degrees(180), anchor: .center)
53 | .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
54 | .animation(.easeOut(duration: 0.2))
55 | .clipped()
56 | }
57 |
58 | }
59 |
60 | }
61 | }
62 |
63 | func getYLegendSafe(height:Int)->CGFloat{
64 | if let legend = getYLegend() {
65 | return CGFloat(legend[height])
66 | }
67 | return 0
68 | }
69 |
70 | func getYposition(height: Int)-> CGFloat {
71 | if let legend = getYLegend() {
72 | return (self.frame.height-((CGFloat(legend[height]) - min)*self.stepHeight))-(self.frame.height/2)
73 | }
74 | return 0
75 |
76 | }
77 |
78 | func line(atHeight: CGFloat, width: CGFloat) -> Path {
79 | var hLine = Path()
80 | hLine.move(to: CGPoint(x:5, y: (atHeight-min)*stepHeight))
81 | hLine.addLine(to: CGPoint(x: width, y: (atHeight-min)*stepHeight))
82 | return hLine
83 | }
84 |
85 | func getYLegend() -> [Double]? {
86 | let points = self.data.onlyPoints()
87 | guard let max = points.max() else { return nil }
88 | guard let min = points.min() else { return nil }
89 | let step = Double(max - min)/4
90 | return [min+step * 0, min+step * 1, min+step * 2, min+step * 3, min+step * 4]
91 | }
92 | }
93 |
94 | struct Legend_Previews: PreviewProvider {
95 | static var previews: some View {
96 | GeometryReader{ geometry in
97 | Legend(data: ChartData(points: [0.2,0.4,1.4,4.5]), frame: .constant(geometry.frame(in: .local)), hideHorizontalLines: .constant(false))
98 | }.frame(width: 320, height: 200)
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/Sources/SwiftUICharts/LineChart/Line.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Line.swift
3 | // LineChart
4 | //
5 | // Created by András Samu on 2019. 08. 30..
6 | // Copyright © 2019. András Samu. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public struct Line: View {
12 | @ObservedObject var data: ChartData
13 | @Binding var frame: CGRect
14 | @Binding var touchLocation: CGPoint
15 | @Binding var showIndicator: Bool
16 | @Binding var minDataValue: Double?
17 | @Binding var maxDataValue: Double?
18 | @State private var showFull: Bool = false
19 | @State var showBackground: Bool = true
20 | var gradient: GradientColor = GradientColor(start: Colors.GradientPurple, end: Colors.GradientNeonBlue)
21 | var index:Int = 0
22 | let padding:CGFloat = 30
23 | var curvedLines: Bool = true
24 | var stepWidth: CGFloat {
25 | if data.points.count < 2 {
26 | return 0
27 | }
28 | return frame.size.width / CGFloat(data.points.count-1)
29 | }
30 | var stepHeight: CGFloat {
31 | var min: Double?
32 | var max: Double?
33 | let points = self.data.onlyPoints()
34 | if minDataValue != nil && maxDataValue != nil {
35 | min = minDataValue!
36 | max = maxDataValue!
37 | }else if let minPoint = points.min(), let maxPoint = points.max(), minPoint != maxPoint {
38 | min = minPoint
39 | max = maxPoint
40 | }else {
41 | return 0
42 | }
43 | if let min = min, let max = max, min != max {
44 | if (min <= 0){
45 | return (frame.size.height-padding) / CGFloat(max - min)
46 | }else{
47 | return (frame.size.height-padding) / CGFloat(max - min)
48 | }
49 | }
50 | return 0
51 | }
52 | var path: Path {
53 | let points = self.data.onlyPoints()
54 | return curvedLines ? Path.quadCurvedPathWithPoints(points: points, step: CGPoint(x: stepWidth, y: stepHeight), globalOffset: minDataValue) : Path.linePathWithPoints(points: points, step: CGPoint(x: stepWidth, y: stepHeight))
55 | }
56 | var closedPath: Path {
57 | let points = self.data.onlyPoints()
58 | return curvedLines ? Path.quadClosedCurvedPathWithPoints(points: points, step: CGPoint(x: stepWidth, y: stepHeight), globalOffset: minDataValue) : Path.closedLinePathWithPoints(points: points, step: CGPoint(x: stepWidth, y: stepHeight))
59 | }
60 |
61 | public var body: some View {
62 | ZStack {
63 | if(self.showFull && self.showBackground){
64 | self.closedPath
65 | .fill(LinearGradient(gradient: Gradient(colors: [Colors.GradientUpperBlue, .white]), startPoint: .bottom, endPoint: .top))
66 | .rotationEffect(.degrees(180), anchor: .center)
67 | .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
68 | .transition(.opacity)
69 | .animation(.easeIn(duration: 1.6))
70 | }
71 | self.path
72 | .trim(from: 0, to: self.showFull ? 1:0)
73 | .stroke(LinearGradient(gradient: gradient.getGradient(), startPoint: .leading, endPoint: .trailing) ,style: StrokeStyle(lineWidth: 3, lineJoin: .round))
74 | .rotationEffect(.degrees(180), anchor: .center)
75 | .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
76 | .animation(Animation.easeOut(duration: 1.2).delay(Double(self.index)*0.4))
77 | .onAppear {
78 | self.showFull = true
79 | }
80 | .onDisappear {
81 | self.showFull = false
82 | }
83 | if(self.showIndicator) {
84 | IndicatorPoint()
85 | .position(self.getClosestPointOnPath(touchLocation: self.touchLocation))
86 | .rotationEffect(.degrees(180), anchor: .center)
87 | .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
88 | }
89 | }
90 | }
91 |
92 | func getClosestPointOnPath(touchLocation: CGPoint) -> CGPoint {
93 | let closest = self.path.point(to: touchLocation.x)
94 | return closest
95 | }
96 |
97 | }
98 |
99 | struct Line_Previews: PreviewProvider {
100 | static var previews: some View {
101 | GeometryReader{ geometry in
102 | 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))
103 | }.frame(width: 320, height: 160)
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/Sources/SwiftUICharts/LineChart/LineChartView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LineCard.swift
3 | // LineChart
4 | //
5 | // Created by András Samu on 2019. 08. 31..
6 | // Copyright © 2019. András Samu. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public struct LineChartView: View {
12 | @Environment(\.colorScheme) var colorScheme: ColorScheme
13 | @ObservedObject var data:ChartData
14 | public var title: String
15 | public var legend: String?
16 | public var style: ChartStyle
17 | public var darkModeStyle: ChartStyle
18 |
19 | public var formSize:CGSize
20 | public var dropShadow: Bool
21 | public var valueSpecifier:String
22 |
23 | @State private var touchLocation:CGPoint = .zero
24 | @State private var showIndicatorDot: Bool = false
25 | @State private var currentValue: Double = 2 {
26 | didSet{
27 | if (oldValue != self.currentValue && showIndicatorDot) {
28 | HapticFeedback.playSelection()
29 | }
30 |
31 | }
32 | }
33 | var frame = CGSize(width: 180, height: 120)
34 | private var rateValue: Int?
35 |
36 | public init(data: [Double],
37 | title: String,
38 | legend: String? = nil,
39 | style: ChartStyle = Styles.lineChartStyleOne,
40 | form: CGSize? = ChartForm.medium,
41 | rateValue: Int?,
42 | dropShadow: Bool? = true,
43 | valueSpecifier: String? = "%.1f") {
44 |
45 | self.data = ChartData(points: data)
46 | self.title = title
47 | self.legend = legend
48 | self.style = style
49 | self.darkModeStyle = style.darkModeStyle != nil ? style.darkModeStyle! : Styles.lineViewDarkMode
50 | self.formSize = form!
51 | frame = CGSize(width: self.formSize.width, height: self.formSize.height/2)
52 | self.dropShadow = dropShadow!
53 | self.valueSpecifier = valueSpecifier!
54 | self.rateValue = rateValue
55 | }
56 |
57 | public var body: some View {
58 | ZStack(alignment: .center){
59 | RoundedRectangle(cornerRadius: 20)
60 | .fill(self.colorScheme == .dark ? self.darkModeStyle.backgroundColor : self.style.backgroundColor)
61 | .frame(width: frame.width, height: 240, alignment: .center)
62 | .shadow(color: self.style.dropShadowColor, radius: self.dropShadow ? 8 : 0)
63 | VStack(alignment: .leading){
64 | if(!self.showIndicatorDot){
65 | VStack(alignment: .leading, spacing: 8){
66 | Text(self.title)
67 | .font(.title)
68 | .bold()
69 | .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor)
70 | if (self.legend != nil){
71 | Text(self.legend!)
72 | .font(.callout)
73 | .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor :self.style.legendTextColor)
74 | }
75 | HStack {
76 |
77 | if let rateValue = self.rateValue
78 | {
79 | if (rateValue ?? 0 >= 0){
80 | Image(systemName: "arrow.up")
81 | }else{
82 | Image(systemName: "arrow.down")
83 | }
84 | Text("\(rateValue!)%")
85 | }
86 | }
87 | }
88 | .transition(.opacity)
89 | .animation(.easeIn(duration: 0.1))
90 | .padding([.leading, .top])
91 | }else{
92 | HStack{
93 | Spacer()
94 | Text("\(self.currentValue, specifier: self.valueSpecifier)")
95 | .font(.system(size: 41, weight: .bold, design: .default))
96 | .offset(x: 0, y: 30)
97 | Spacer()
98 | }
99 | .transition(.scale)
100 | }
101 | Spacer()
102 | GeometryReader{ geometry in
103 | Line(data: self.data,
104 | frame: .constant(geometry.frame(in: .local)),
105 | touchLocation: self.$touchLocation,
106 | showIndicator: self.$showIndicatorDot,
107 | minDataValue: .constant(nil),
108 | maxDataValue: .constant(nil)
109 | )
110 | }
111 | .frame(width: frame.width, height: frame.height)
112 | .clipShape(RoundedRectangle(cornerRadius: 20))
113 | .offset(x: 0, y: 0)
114 | }.frame(width: self.formSize.width, height: self.formSize.height)
115 | }
116 | .gesture(DragGesture()
117 | .onChanged({ value in
118 | self.touchLocation = value.location
119 | self.showIndicatorDot = true
120 | self.getClosestDataPoint(toPoint: value.location, width:self.frame.width, height: self.frame.height)
121 | })
122 | .onEnded({ value in
123 | self.showIndicatorDot = false
124 | })
125 | )
126 | }
127 |
128 | @discardableResult func getClosestDataPoint(toPoint: CGPoint, width:CGFloat, height: CGFloat) -> CGPoint {
129 | let points = self.data.onlyPoints()
130 | let stepWidth: CGFloat = width / CGFloat(points.count-1)
131 | let stepHeight: CGFloat = height / CGFloat(points.max()! + points.min()!)
132 |
133 | let index:Int = Int(round((toPoint.x)/stepWidth))
134 | if (index >= 0 && index < points.count){
135 | self.currentValue = points[index]
136 | return CGPoint(x: CGFloat(index)*stepWidth, y: CGFloat(points[index])*stepHeight)
137 | }
138 | return .zero
139 | }
140 | }
141 |
142 | struct WidgetView_Previews: PreviewProvider {
143 | static var previews: some View {
144 | Group {
145 | LineChartView(data: [8,23,54,32,12,37,7,23,43], title: "Line chart", legend: "Basic")
146 | .environment(\.colorScheme, .light)
147 |
148 | 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")
149 | .environment(\.colorScheme, .light)
150 | }
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/Sources/SwiftUICharts/LineChart/LineView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LineView.swift
3 | // LineChart
4 | //
5 | // Created by András Samu on 2019. 09. 02..
6 | // Copyright © 2019. András Samu. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public struct LineView: View {
12 | @ObservedObject var data: ChartData
13 | public var title: String?
14 | public var legend: String?
15 | public var style: ChartStyle
16 | public var darkModeStyle: ChartStyle
17 | public var valueSpecifier: String
18 | public var legendSpecifier: String
19 |
20 | @Environment(\.colorScheme) var colorScheme: ColorScheme
21 | @State private var showLegend = false
22 | @State private var dragLocation:CGPoint = .zero
23 | @State private var indicatorLocation:CGPoint = .zero
24 | @State private var closestPoint: CGPoint = .zero
25 | @State private var opacity:Double = 0
26 | @State private var currentDataNumber: Double = 0
27 | @State private var hideHorizontalLines: Bool = false
28 |
29 | public init(data: [Double],
30 | title: String? = nil,
31 | legend: String? = nil,
32 | style: ChartStyle = Styles.lineChartStyleOne,
33 | valueSpecifier: String? = "%.1f",
34 | legendSpecifier: String? = "%.2f") {
35 |
36 | self.data = ChartData(points: data)
37 | self.title = title
38 | self.legend = legend
39 | self.style = style
40 | self.valueSpecifier = valueSpecifier!
41 | self.legendSpecifier = legendSpecifier!
42 | self.darkModeStyle = style.darkModeStyle != nil ? style.darkModeStyle! : Styles.lineViewDarkMode
43 | }
44 |
45 | public var body: some View {
46 | GeometryReader{ geometry in
47 | VStack(alignment: .leading, spacing: 8) {
48 | Group{
49 | if (self.title != nil){
50 | Text(self.title!)
51 | .font(.title)
52 | .bold().foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor)
53 | }
54 | if (self.legend != nil){
55 | Text(self.legend!)
56 | .font(.callout)
57 | .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor)
58 | }
59 | }.offset(x: 0, y: 20)
60 | ZStack{
61 | GeometryReader{ reader in
62 | Rectangle()
63 | .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.backgroundColor : self.style.backgroundColor)
64 | if(self.showLegend){
65 | Legend(data: self.data,
66 | frame: .constant(reader.frame(in: .local)), hideHorizontalLines: self.$hideHorizontalLines, specifier: legendSpecifier)
67 | .transition(.opacity)
68 | .animation(Animation.easeOut(duration: 1).delay(1))
69 | }
70 | Line(data: self.data,
71 | frame: .constant(CGRect(x: 0, y: 0, width: reader.frame(in: .local).width - 30, height: reader.frame(in: .local).height + 25)),
72 | touchLocation: self.$indicatorLocation,
73 | showIndicator: self.$hideHorizontalLines,
74 | minDataValue: .constant(nil),
75 | maxDataValue: .constant(nil),
76 | showBackground: false,
77 | gradient: self.style.gradientColor
78 | )
79 | .offset(x: 30, y: 0)
80 | .onAppear(){
81 | self.showLegend = true
82 | }
83 | .onDisappear(){
84 | self.showLegend = false
85 | }
86 | }
87 | .frame(width: geometry.frame(in: .local).size.width, height: 240)
88 | .offset(x: 0, y: 40 )
89 | MagnifierRect(currentNumber: self.$currentDataNumber, valueSpecifier: self.valueSpecifier)
90 | .opacity(self.opacity)
91 | .offset(x: self.dragLocation.x - geometry.frame(in: .local).size.width/2, y: 36)
92 | }
93 | .frame(width: geometry.frame(in: .local).size.width, height: 240)
94 | .gesture(DragGesture()
95 | .onChanged({ value in
96 | self.dragLocation = value.location
97 | self.indicatorLocation = CGPoint(x: max(value.location.x-30,0), y: 32)
98 | self.opacity = 1
99 | self.closestPoint = self.getClosestDataPoint(toPoint: value.location, width: geometry.frame(in: .local).size.width-30, height: 240)
100 | self.hideHorizontalLines = true
101 | })
102 | .onEnded({ value in
103 | self.opacity = 0
104 | self.hideHorizontalLines = false
105 | })
106 | )
107 | }
108 | }
109 | }
110 |
111 | func getClosestDataPoint(toPoint: CGPoint, width:CGFloat, height: CGFloat) -> CGPoint {
112 | let points = self.data.onlyPoints()
113 | let stepWidth: CGFloat = width / CGFloat(points.count-1)
114 | let stepHeight: CGFloat = height / CGFloat(points.max()! + points.min()!)
115 |
116 | let index:Int = Int(floor((toPoint.x-15)/stepWidth))
117 | if (index >= 0 && index < points.count){
118 | self.currentDataNumber = points[index]
119 | return CGPoint(x: CGFloat(index)*stepWidth, y: CGFloat(points[index])*stepHeight)
120 | }
121 | return .zero
122 | }
123 | }
124 |
125 | struct LineView_Previews: PreviewProvider {
126 | static var previews: some View {
127 | Group {
128 | LineView(data: [8,23,54,32,12,37,7,23,43], title: "Full chart", style: Styles.lineChartStyleOne)
129 |
130 | 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)
131 |
132 | }
133 | }
134 | }
135 |
136 |
--------------------------------------------------------------------------------
/Sources/SwiftUICharts/LineChart/MagnifierRect.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MagnifierRect.swift
3 | //
4 | //
5 | // Created by Samu András on 2020. 03. 04..
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct MagnifierRect: View {
11 | @Binding var currentNumber: Double
12 | var valueSpecifier:String
13 | @Environment(\.colorScheme) var colorScheme: ColorScheme
14 | public var body: some View {
15 | ZStack{
16 | Text("\(self.currentNumber, specifier: valueSpecifier)")
17 | .font(.system(size: 18, weight: .bold))
18 | .offset(x: 0, y:-110)
19 | .foregroundColor(self.colorScheme == .dark ? Color.white : Color.black)
20 | if (self.colorScheme == .dark ){
21 | RoundedRectangle(cornerRadius: 16)
22 | .stroke(Color.white, lineWidth: self.colorScheme == .dark ? 2 : 0)
23 | .frame(width: 60, height: 260)
24 | }else{
25 | RoundedRectangle(cornerRadius: 16)
26 | .frame(width: 60, height: 280)
27 | .foregroundColor(Color.white)
28 | .shadow(color: Colors.LegendText, radius: 12, x: 0, y: 6 )
29 | .blendMode(.multiply)
30 | }
31 | }
32 | .offset(x: 0, y: -15)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/SwiftUICharts/LineChart/MultiLineChartView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Samu András on 2020. 02. 19..
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct MultiLineChartView: View {
11 | @Environment(\.colorScheme) var colorScheme: ColorScheme
12 | var data:[MultiLineChartData]
13 | public var title: String
14 | public var legend: String?
15 | public var style: ChartStyle
16 | public var darkModeStyle: ChartStyle
17 | public var formSize: CGSize
18 | public var dropShadow: Bool
19 | public var valueSpecifier:String
20 |
21 | @State private var touchLocation:CGPoint = .zero
22 | @State private var showIndicatorDot: Bool = false
23 | @State private var currentValue: Double = 2 {
24 | didSet{
25 | if (oldValue != self.currentValue && showIndicatorDot) {
26 | HapticFeedback.playSelection()
27 | }
28 |
29 | }
30 | }
31 |
32 | var globalMin:Double {
33 | if let min = data.flatMap({$0.onlyPoints()}).min() {
34 | return min
35 | }
36 | return 0
37 | }
38 |
39 | var globalMax:Double {
40 | if let max = data.flatMap({$0.onlyPoints()}).max() {
41 | return max
42 | }
43 | return 0
44 | }
45 |
46 | var frame = CGSize(width: 180, height: 120)
47 | private var rateValue: Int?
48 |
49 | public init(data: [([Double], GradientColor)],
50 | title: String,
51 | legend: String? = nil,
52 | style: ChartStyle = Styles.lineChartStyleOne,
53 | form: CGSize = ChartForm.medium,
54 | rateValue: Int? = nil,
55 | dropShadow: Bool = true,
56 | valueSpecifier: String = "%.1f") {
57 |
58 | self.data = data.map({ MultiLineChartData(points: $0.0, gradient: $0.1)})
59 | self.title = title
60 | self.legend = legend
61 | self.style = style
62 | self.darkModeStyle = style.darkModeStyle != nil ? style.darkModeStyle! : Styles.lineViewDarkMode
63 | self.formSize = form
64 | frame = CGSize(width: self.formSize.width, height: self.formSize.height/2)
65 | self.rateValue = rateValue
66 | self.dropShadow = dropShadow
67 | self.valueSpecifier = valueSpecifier
68 | }
69 |
70 | public var body: some View {
71 | ZStack(alignment: .center){
72 | RoundedRectangle(cornerRadius: 20)
73 | .fill(self.colorScheme == .dark ? self.darkModeStyle.backgroundColor : self.style.backgroundColor)
74 | .frame(width: frame.width, height: 240, alignment: .center)
75 | .shadow(radius: self.dropShadow ? 8 : 0)
76 | VStack(alignment: .leading){
77 | if(!self.showIndicatorDot){
78 | VStack(alignment: .leading, spacing: 8){
79 | Text(self.title)
80 | .font(.title)
81 | .bold()
82 | .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor)
83 | if (self.legend != nil){
84 | Text(self.legend!)
85 | .font(.callout)
86 | .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor)
87 | }
88 | HStack {
89 | if (rateValue ?? 0 >= 0){
90 | Image(systemName: "arrow.up")
91 | }else{
92 | Image(systemName: "arrow.down")
93 | }
94 | Text("\(rateValue ?? 0)%")
95 | }
96 | }
97 | .transition(.opacity)
98 | .animation(.easeIn(duration: 0.1))
99 | .padding([.leading, .top])
100 | }else{
101 | HStack{
102 | Spacer()
103 | Text("\(self.currentValue, specifier: self.valueSpecifier)")
104 | .font(.system(size: 41, weight: .bold, design: .default))
105 | .offset(x: 0, y: 30)
106 | Spacer()
107 | }
108 | .transition(.scale)
109 | }
110 | Spacer()
111 | GeometryReader{ geometry in
112 | ZStack{
113 | ForEach(0.. CGPoint {
144 | // let points = self.data.onlyPoints()
145 | // let stepWidth: CGFloat = width / CGFloat(points.count-1)
146 | // let stepHeight: CGFloat = height / CGFloat(points.max()! + points.min()!)
147 | //
148 | // let index:Int = Int(round((toPoint.x)/stepWidth))
149 | // if (index >= 0 && index < points.count){
150 | // self.currentValue = points[index]
151 | // return CGPoint(x: CGFloat(index)*stepWidth, y: CGFloat(points[index])*stepHeight)
152 | // }
153 | // return .zero
154 | // }
155 | }
156 |
157 | struct MultiWidgetView_Previews: PreviewProvider {
158 | static var previews: some View {
159 | Group {
160 | MultiLineChartView(data: [([8,23,54,32,12,37,7,23,43], GradientColors.orange)], title: "Line chart", legend: "Basic")
161 | .environment(\.colorScheme, .light)
162 | }
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/Sources/SwiftUICharts/LineChart/Path+QuadCurve.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by xspyhack on 2020/1/21.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension Path {
11 | func trimmedPath(for percent: CGFloat) -> Path {
12 | // percent difference between points
13 | let boundsDistance: CGFloat = 0.001
14 | let completion: CGFloat = 1 - boundsDistance
15 |
16 | let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent)
17 |
18 | let start = pct > completion ? completion : pct - boundsDistance
19 | let end = pct > completion ? 1 : pct + boundsDistance
20 | return trimmedPath(from: start, to: end)
21 | }
22 |
23 | func point(for percent: CGFloat) -> CGPoint {
24 | let path = trimmedPath(for: percent)
25 | return CGPoint(x: path.boundingRect.midX, y: path.boundingRect.midY)
26 | }
27 |
28 | func point(to maxX: CGFloat) -> CGPoint {
29 | let total = length
30 | let sub = length(to: maxX)
31 | let percent = sub / total
32 | return point(for: percent)
33 | }
34 |
35 | var length: CGFloat {
36 | var ret: CGFloat = 0.0
37 | var start: CGPoint?
38 | var point = CGPoint.zero
39 |
40 | forEach { ele in
41 | switch ele {
42 | case .move(let to):
43 | if start == nil {
44 | start = to
45 | }
46 | point = to
47 | case .line(let to):
48 | ret += point.line(to: to)
49 | point = to
50 | case .quadCurve(let to, let control):
51 | ret += point.quadCurve(to: to, control: control)
52 | point = to
53 | case .curve(let to, let control1, let control2):
54 | ret += point.curve(to: to, control1: control1, control2: control2)
55 | point = to
56 | case .closeSubpath:
57 | if let to = start {
58 | ret += point.line(to: to)
59 | point = to
60 | }
61 | start = nil
62 | }
63 | }
64 | return ret
65 | }
66 |
67 | func length(to maxX: CGFloat) -> CGFloat {
68 | var ret: CGFloat = 0.0
69 | var start: CGPoint?
70 | var point = CGPoint.zero
71 | var finished = false
72 |
73 | forEach { ele in
74 | if finished {
75 | return
76 | }
77 | switch ele {
78 | case .move(let to):
79 | if to.x > maxX {
80 | finished = true
81 | return
82 | }
83 | if start == nil {
84 | start = to
85 | }
86 | point = to
87 | case .line(let to):
88 | if to.x > maxX {
89 | finished = true
90 | ret += point.line(to: to, x: maxX)
91 | return
92 | }
93 | ret += point.line(to: to)
94 | point = to
95 | case .quadCurve(let to, let control):
96 | if to.x > maxX {
97 | finished = true
98 | ret += point.quadCurve(to: to, control: control, x: maxX)
99 | return
100 | }
101 | ret += point.quadCurve(to: to, control: control)
102 | point = to
103 | case .curve(let to, let control1, let control2):
104 | if to.x > maxX {
105 | finished = true
106 | ret += point.curve(to: to, control1: control1, control2: control2, x: maxX)
107 | return
108 | }
109 | ret += point.curve(to: to, control1: control1, control2: control2)
110 | point = to
111 | case .closeSubpath:
112 | fatalError("Can't include closeSubpath")
113 | }
114 | }
115 | return ret
116 | }
117 |
118 | static func quadCurvedPathWithPoints(points:[Double], step:CGPoint, globalOffset: Double? = nil) -> Path {
119 | var path = Path()
120 | if (points.count < 2){
121 | return path
122 | }
123 | let offset = globalOffset ?? points.min()!
124 | // guard let offset = points.min() else { return path }
125 | var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
126 | path.move(to: p1)
127 | for pointIndex in 1.. Path {
138 | var path = Path()
139 | if (points.count < 2){
140 | return path
141 | }
142 | let offset = globalOffset ?? points.min()!
143 |
144 | // guard let offset = points.min() else { return path }
145 | path.move(to: .zero)
146 | var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
147 | path.addLine(to: p1)
148 | for pointIndex in 1.. Path {
161 | var path = Path()
162 | if (points.count < 2){
163 | return path
164 | }
165 | guard let offset = points.min() else { return path }
166 | let p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
167 | path.move(to: p1)
168 | for pointIndex in 1.. Path {
176 | var path = Path()
177 | if (points.count < 2){
178 | return path
179 | }
180 | guard let offset = points.min() else { return path }
181 | var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
182 | path.move(to: p1)
183 | for pointIndex in 1.. CGPoint {
196 | let a = (to.y - self.y) / (to.x - self.x)
197 | let y = self.y + (x - self.x) * a
198 | return CGPoint(x: x, y: y)
199 | }
200 |
201 | func line(to: CGPoint) -> CGFloat {
202 | dist(to: to)
203 | }
204 |
205 | func line(to: CGPoint, x: CGFloat) -> CGFloat {
206 | dist(to: point(to: to, x: x))
207 | }
208 |
209 | func quadCurve(to: CGPoint, control: CGPoint) -> CGFloat {
210 | var dist: CGFloat = 0
211 | let steps: CGFloat = 100
212 |
213 | for i in 0.. CGFloat {
225 | var dist: CGFloat = 0
226 | let steps: CGFloat = 100
227 |
228 | for i in 0..= x {
235 | return dist
236 | } else if b.x > x {
237 | dist += a.line(to: b, x: x)
238 | return dist
239 | } else if b.x == x {
240 | dist += a.line(to: b)
241 | return dist
242 | }
243 |
244 | dist += a.line(to: b)
245 | }
246 | return dist
247 | }
248 |
249 | func point(to: CGPoint, t: CGFloat, control: CGPoint) -> CGPoint {
250 | let x = CGPoint.value(x: self.x, y: to.x, t: t, c: control.x)
251 | let y = CGPoint.value(x: self.y, y: to.y, t: t, c: control.y)
252 |
253 | return CGPoint(x: x, y: y)
254 | }
255 |
256 | func curve(to: CGPoint, control1: CGPoint, control2: CGPoint) -> CGFloat {
257 | var dist: CGFloat = 0
258 | let steps: CGFloat = 100
259 |
260 | for i in 0.. CGFloat {
274 | var dist: CGFloat = 0
275 | let steps: CGFloat = 100
276 |
277 | for i in 0..= x {
285 | return dist
286 | } else if b.x > x {
287 | dist += a.line(to: b, x: x)
288 | return dist
289 | } else if b.x == x {
290 | dist += a.line(to: b)
291 | return dist
292 | }
293 |
294 | dist += a.line(to: b)
295 | }
296 |
297 | return dist
298 | }
299 |
300 | func point(to: CGPoint, t: CGFloat, control1: CGPoint, control2: CGPoint) -> CGPoint {
301 | let x = CGPoint.value(x: self.x, y: to.x, t: t, c1: control1.x, c2: control2.x)
302 | let y = CGPoint.value(x: self.y, y: to.y, t: t, c1: control1.y, c2: control2.x)
303 |
304 | return CGPoint(x: x, y: y)
305 | }
306 |
307 | static func value(x: CGFloat, y: CGFloat, t: CGFloat, c: CGFloat) -> CGFloat {
308 | var value: CGFloat = 0.0
309 | // (1-t)^2 * p0 + 2 * (1-t) * t * c1 + t^2 * p1
310 | value += pow(1-t, 2) * x
311 | value += 2 * (1-t) * t * c
312 | value += pow(t, 2) * y
313 | return value
314 | }
315 |
316 | static func value(x: CGFloat, y: CGFloat, t: CGFloat, c1: CGFloat, c2: CGFloat) -> CGFloat {
317 | var value: CGFloat = 0.0
318 | // (1-t)^3 * p0 + 3 * (1-t)^2 * t * c1 + 3 * (1-t) * t^2 * c2 + t^3 * p1
319 | value += pow(1-t, 3) * x
320 | value += 3 * pow(1-t, 2) * t * c1
321 | value += 3 * (1-t) * pow(t, 2) * c2
322 | value += pow(t, 3) * y
323 | return value
324 | }
325 |
326 | static func getMidPoint(point1: CGPoint, point2: CGPoint) -> CGPoint {
327 | return CGPoint(
328 | x: point1.x + (point2.x - point1.x) / 2,
329 | y: point1.y + (point2.y - point1.y) / 2
330 | )
331 | }
332 |
333 | func dist(to: CGPoint) -> CGFloat {
334 | return sqrt((pow(self.x - to.x, 2) + pow(self.y - to.y, 2)))
335 | }
336 |
337 | static func midPointForPoints(p1:CGPoint, p2:CGPoint) -> CGPoint {
338 | return CGPoint(x:(p1.x + p2.x) / 2,y: (p1.y + p2.y) / 2)
339 | }
340 |
341 | static func controlPointForPoints(p1:CGPoint, p2:CGPoint) -> CGPoint {
342 | var controlPoint = CGPoint.midPointForPoints(p1:p1, p2:p2)
343 | let diffY = abs(p2.y - controlPoint.y)
344 |
345 | if (p1.y < p2.y){
346 | controlPoint.y += diffY
347 | } else if (p1.y > p2.y) {
348 | controlPoint.y -= diffY
349 | }
350 | return controlPoint
351 | }
352 | }
353 |
354 |
--------------------------------------------------------------------------------
/Sources/SwiftUICharts/PieChart/PieChartCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PieChartCell.swift
3 | // ChartView
4 | //
5 | // Created by András Samu on 2019. 06. 12..
6 | // Copyright © 2019. András Samu. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct PieSlice: Identifiable {
12 | var id = UUID()
13 | var startDeg: Double
14 | var endDeg: Double
15 | var value: Double
16 | var normalizedValue: Double
17 | }
18 |
19 | public struct PieChartCell : View {
20 | @State private var show:Bool = false
21 | var rect: CGRect
22 | var radius: CGFloat {
23 | return min(rect.width, rect.height)/2
24 | }
25 | var startDeg: Double
26 | var endDeg: Double
27 | var path: Path {
28 | var path = Path()
29 | path.addArc(center:rect.mid , radius:self.radius, startAngle: Angle(degrees: self.startDeg), endAngle: Angle(degrees: self.endDeg), clockwise: false)
30 | path.addLine(to: rect.mid)
31 | path.closeSubpath()
32 | return path
33 | }
34 | var index: Int
35 | var backgroundColor:Color
36 | var accentColor:Color
37 | public var body: some View {
38 | path
39 | .fill()
40 | .foregroundColor(self.accentColor)
41 | .overlay(path.stroke(self.backgroundColor, lineWidth: 2))
42 | .scaleEffect(self.show ? 1 : 0)
43 | .animation(Animation.spring().delay(Double(self.index) * 0.04))
44 | .onAppear(){
45 | self.show = true
46 | }
47 | }
48 | }
49 |
50 | extension CGRect {
51 | var mid: CGPoint {
52 | return CGPoint(x:self.midX, y: self.midY)
53 | }
54 | }
55 |
56 | #if DEBUG
57 | struct PieChartCell_Previews : PreviewProvider {
58 | static var previews: some View {
59 | GeometryReader { geometry in
60 | PieChartCell(rect: geometry.frame(in: .local),startDeg: 0.0,endDeg: 90.0, index: 0, backgroundColor: Color(red: 252.0/255.0, green: 236.0/255.0, blue: 234.0/255.0), accentColor: Color(red: 225.0/255.0, green: 97.0/255.0, blue: 76.0/255.0))
61 | }.frame(width:100, height:100)
62 |
63 | }
64 | }
65 | #endif
66 |
--------------------------------------------------------------------------------
/Sources/SwiftUICharts/PieChart/PieChartHelpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by 曾文志 on 2020/7/30.
6 | //
7 |
8 | import SwiftUI
9 |
10 | func isPointInCircle(point: CGPoint, circleRect: CGRect) -> Bool {
11 | let r = min(circleRect.width, circleRect.height) / 2
12 | let center = CGPoint(x: circleRect.midX, y: circleRect.midY)
13 | let dx = point.x - center.x
14 | let dy = point.y - center.y
15 | let distance = sqrt(dx * dx + dy * dy)
16 | return distance <= r
17 | }
18 |
19 | func degree(for point: CGPoint, inCircleRect circleRect: CGRect) -> Double {
20 | let center = CGPoint(x: circleRect.midX, y: circleRect.midY)
21 | let dx = point.x - center.x
22 | let dy = point.y - center.y
23 | let acuteDegree = Double(atan(dy / dx)) * (180 / .pi)
24 |
25 | let isInBottomRight = dx >= 0 && dy >= 0
26 | let isInBottomLeft = dx <= 0 && dy >= 0
27 | let isInTopLeft = dx <= 0 && dy <= 0
28 | let isInTopRight = dx >= 0 && dy <= 0
29 |
30 | if isInBottomRight {
31 | return acuteDegree
32 | } else if isInBottomLeft {
33 | return 180 - abs(acuteDegree)
34 | } else if isInTopLeft {
35 | return 180 + abs(acuteDegree)
36 | } else if isInTopRight {
37 | return 360 - abs(acuteDegree)
38 | }
39 |
40 | return 0
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/SwiftUICharts/PieChart/PieChartRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PieChartRow.swift
3 | // ChartView
4 | //
5 | // Created by András Samu on 2019. 06. 12..
6 | // Copyright © 2019. András Samu. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public struct PieChartRow : View {
12 | var data: [Double]
13 | var backgroundColor: Color
14 | var accentColor: Color
15 | var slices: [PieSlice] {
16 | var tempSlices:[PieSlice] = []
17 | var lastEndDeg:Double = 0
18 | let maxValue = data.reduce(0, +)
19 | for slice in data {
20 | let normalized:Double = Double(slice)/Double(maxValue)
21 | let startDeg = lastEndDeg
22 | let endDeg = lastEndDeg + (normalized * 360)
23 | lastEndDeg = endDeg
24 | tempSlices.append(PieSlice(startDeg: startDeg, endDeg: endDeg, value: slice, normalizedValue: normalized))
25 | }
26 | return tempSlices
27 | }
28 |
29 | @Binding var showValue: Bool
30 | @Binding var currentValue: Double
31 |
32 | @State private var currentTouchedIndex = -1 {
33 | didSet {
34 | if oldValue != currentTouchedIndex {
35 | showValue = currentTouchedIndex != -1
36 | currentValue = showValue ? slices[currentTouchedIndex].value : 0
37 | }
38 | }
39 | }
40 |
41 | public var body: some View {
42 | GeometryReader { geometry in
43 | ZStack{
44 | ForEach(0.. touchDegree }) ?? -1
57 | } else {
58 | self.currentTouchedIndex = -1
59 | }
60 | })
61 | .onEnded({ value in
62 | self.currentTouchedIndex = -1
63 | }))
64 | }
65 | }
66 | }
67 |
68 | #if DEBUG
69 | struct PieChartRow_Previews : PreviewProvider {
70 | static var previews: some View {
71 | Group {
72 | 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))
73 | .frame(width: 100, height: 100)
74 | 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))
75 | .frame(width: 100, height: 100)
76 | }
77 | }
78 | }
79 | #endif
80 |
--------------------------------------------------------------------------------
/Sources/SwiftUICharts/PieChart/PieChartView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PieChartView.swift
3 | // ChartView
4 | //
5 | // Created by András Samu on 2019. 06. 12..
6 | // Copyright © 2019. András Samu. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public struct PieChartView : View {
12 | public var data: [Double]
13 | public var title: String
14 | public var legend: String?
15 | public var style: ChartStyle
16 | public var formSize:CGSize
17 | public var dropShadow: Bool
18 | public var valueSpecifier:String
19 |
20 | @State private var showValue = false
21 | @State private var currentValue: Double = 0 {
22 | didSet{
23 | if(oldValue != self.currentValue && self.showValue) {
24 | HapticFeedback.playSelection()
25 | }
26 | }
27 | }
28 |
29 | public init(data: [Double], title: String, legend: String? = nil, style: ChartStyle = Styles.pieChartStyleOne, form: CGSize? = ChartForm.medium, dropShadow: Bool? = true, valueSpecifier: String? = "%.1f"){
30 | self.data = data
31 | self.title = title
32 | self.legend = legend
33 | self.style = style
34 | self.formSize = form!
35 | if self.formSize == ChartForm.large {
36 | self.formSize = ChartForm.extraLarge
37 | }
38 | self.dropShadow = dropShadow!
39 | self.valueSpecifier = valueSpecifier!
40 | }
41 |
42 | public var body: some View {
43 | ZStack{
44 | Rectangle()
45 | .fill(self.style.backgroundColor)
46 | .cornerRadius(20)
47 | .shadow(color: self.style.dropShadowColor, radius: self.dropShadow ? 12 : 0)
48 | VStack(alignment: .leading){
49 | HStack{
50 | if(!showValue){
51 | Text(self.title)
52 | .font(.headline)
53 | .foregroundColor(self.style.textColor)
54 | }else{
55 | Text("\(self.currentValue, specifier: self.valueSpecifier)")
56 | .font(.headline)
57 | .foregroundColor(self.style.textColor)
58 | }
59 | Spacer()
60 | Image(systemName: "chart.pie.fill")
61 | .imageScale(.large)
62 | .foregroundColor(self.style.legendTextColor)
63 | }.padding()
64 | PieChartRow(data: data, backgroundColor: self.style.backgroundColor, accentColor: self.style.accentColor, showValue: $showValue, currentValue: $currentValue)
65 | .foregroundColor(self.style.accentColor).padding(self.legend != nil ? 0 : 12).offset(y:self.legend != nil ? 0 : -10)
66 | if(self.legend != nil) {
67 | Text(self.legend!)
68 | .font(.headline)
69 | .foregroundColor(self.style.legendTextColor)
70 | .padding()
71 | }
72 |
73 | }
74 | }.frame(width: self.formSize.width, height: self.formSize.height)
75 | }
76 | }
77 |
78 | #if DEBUG
79 | struct PieChartView_Previews : PreviewProvider {
80 | static var previews: some View {
81 | PieChartView(data:[56,78,53,65,54], title: "Title", legend: "Legend")
82 | }
83 | }
84 | #endif
85 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import SwiftUIChartsTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += SwiftUIChartsTests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------
/Tests/SwiftUIChartsTests/SwiftUIChartsTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import SwiftUICharts
3 |
4 | final class SwiftUIChartsTests: XCTestCase {
5 | func testExample() {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 | }
10 |
11 | static var allTests = [
12 | ("testExample", testExample),
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/Tests/SwiftUIChartsTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(SwiftUIChartsTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------