├── .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 | --------------------------------------------------------------------------------