├── .gitattributes ├── Sources ├── Resources │ └── Adsız.gif └── SwiftUIPercentChart │ ├── SwiftUIPercentChart.swift │ └── Helpers.swift ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Tests └── SwiftUIPercentChartTests │ └── SwiftUIPercentChartTests.swift ├── LICENSE ├── README.md └── Package.swift /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /Sources/Resources/Adsız.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devmehmetates/SwiftUIPercentChart/HEAD/Sources/Resources/Adsız.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/SwiftUIPercentChartTests/SwiftUIPercentChartTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftUIPercentChart 3 | 4 | final class SwiftUIPercentChartTests: XCTestCase { 5 | func testExample() throws { 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 | // XCTAssertEqual("Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mehmet ateş 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftUIPercentChart 2 | 3 | Easily create graphs that calculate percentiles 4 | 5 | ## How to install this package 6 | 7 | + Open your project on Xcode 8 | + Go to Project Tab and select "Package Dependencies" 9 | + Click "+" and search this package with use git clone url 10 | + Don't change anything and click Add Package 11 | + The package will be attached to the targeted application 12 | 13 | ## How to use this package 14 | 15 | ```swift 16 | import SwiftUIPercentChart 17 | 18 | struct DemoView: View { 19 | var body: some View { 20 | SwiftUIPercentChart(data: [50, 40, 30, 20, 30, 50, 30, 10, 20, 50], percentValue: 350, theme: .ocean) 21 | .frame(width: screenSize.width * 0.7, height: 10) 22 | } 23 | } 24 | ``` 25 | 26 | ## Demo Images 27 |
28 | Screenshot 2023-03-26 at 6 46 47 PM 29 | Screenshot 2023-03-26 at 6 48 48 PM 30 |
31 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 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: "SwiftUIPercentChart", 8 | platforms: [.iOS(.v15), .macOS(.v12)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, and make them visible to other packages. 11 | .library( 12 | name: "SwiftUIPercentChart", 13 | targets: ["SwiftUIPercentChart"]), 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | // .package(url: /* package url */, from: "1.0.0"), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 22 | .target( 23 | name: "SwiftUIPercentChart", 24 | dependencies: []), 25 | .testTarget( 26 | name: "SwiftUIPercentChartTests", 27 | dependencies: ["SwiftUIPercentChart"]), 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /Sources/SwiftUIPercentChart/SwiftUIPercentChart.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// Creates a horizontal chart that calculates percentile slices 4 | public struct SwiftUIPercentChart: View { 5 | private var data: [Double] = [] 6 | private var percentValue: Double = 0 7 | private var colors: [Color] = [] 8 | 9 | public init(data: [Double] = [], percentValue: Double? = nil, theme: Themes = .dark) { 10 | self.commonInit( 11 | data, 12 | percentValue: calculatePercent(percentValue), 13 | colors: ColorThemes.getColors(by: theme) 14 | ) 15 | } 16 | 17 | public init(data: [Double] = [], percentValue: Double? = nil, theme: [Color]?) { 18 | commonInit( 19 | data, 20 | percentValue: calculatePercent(percentValue), 21 | colors: theme ?? ColorThemes.getColors(by: .currency) 22 | ) 23 | } 24 | 25 | private mutating func commonInit(_ data: [Double], percentValue: Double, colors: [Color]) { 26 | self.data = data 27 | self.percentValue = percentValue 28 | self.colors = colors 29 | } 30 | 31 | private func calculatePercent(_ percentValue: Double?) -> Double { 32 | guard let percentValue else { return 0 } 33 | let sumOfDatas = Double(data.reduce(0, +)) 34 | return max(sumOfDatas, percentValue) 35 | } 36 | 37 | public var body: some View { 38 | GeometryReader { proxy in 39 | HStack(spacing: 0) { 40 | ForEach(Array(zip(data.indices, data)), id: \.0) { index, value in 41 | createRectange(by: index, proxy: proxy) 42 | } 43 | 44 | if percentValue > data.reduce(0, +) { 45 | Spacer() 46 | } 47 | }.background(.quaternary) 48 | .clipShape( 49 | RoundedRectangle(cornerRadius: 16) 50 | ) 51 | } 52 | } 53 | 54 | @ViewBuilder private func createRectange(by index: Int, proxy: GeometryProxy) -> some View { 55 | if let cellRect = getCellRectShape(by: index) { 56 | Rectangle() 57 | .cornerRadius(cellRadius(by: index), corners: cellRect) 58 | .foregroundColor(Color.getColor(colors, index)) 59 | .frame(width: cellWidth(by: index, proxy.size.width)) 60 | } else { 61 | Rectangle() 62 | .foregroundColor(Color.getColor(colors, index)) 63 | .frame(width: cellWidth(by: index, proxy.size.width)) 64 | } 65 | } 66 | 67 | private func getCellRectShape(by index: Int) -> UIRectCorner? { 68 | if index == .zero { 69 | return [.topLeft, .bottomLeft] 70 | } else if index == data.count - 1 { 71 | return [.topRight, .bottomRight] 72 | } 73 | 74 | return nil 75 | } 76 | 77 | private func cellCapacity(by index: Int) -> Double { 78 | return (data[index] * 100) / percentValue 79 | } 80 | 81 | private func cellWidth(by index: Int, _ width: CGFloat) -> Double { 82 | if data[index] == 0 { 83 | return 0 84 | } 85 | return (cellCapacity(by: index) * width) / 100 86 | } 87 | 88 | private func cellRadius(by index: Int) -> Double { 89 | return 500 / cellCapacity(by: index) 90 | } 91 | } 92 | 93 | #if DEBUG 94 | struct SwiftUIPercentChart_Previews : PreviewProvider { 95 | static var previews: some View { 96 | let screenSize = UIScreen.main.bounds 97 | 98 | VStack { 99 | SwiftUIPercentChart(data: [1, 2, 25], percentValue: 10, theme: [.red, .blue, .green]) 100 | .frame(width: screenSize.width * 0.7, height: 10) 101 | 102 | SwiftUIPercentChart(data: [1, 0, 0], theme: .currency) 103 | .frame(width: screenSize.width * 0.7, height: 10) 104 | 105 | ForEach(Themes.allCases, id: \.self) { theme in 106 | VStack(alignment: .leading) { 107 | SwiftUIPercentChart(data: [50, 40, 30, 20, 30, 50, 30, 10, 20, 50], percentValue: 350, theme: theme) 108 | .frame(width: screenSize.width * 0.7, height: 10) 109 | Text(theme.rawValue) 110 | .bold() 111 | }.padding() 112 | .background(.ultraThinMaterial) 113 | .cornerRadius(10) 114 | } 115 | } 116 | } 117 | } 118 | #endif 119 | -------------------------------------------------------------------------------- /Sources/SwiftUIPercentChart/Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIView.swift 3 | // 4 | // 5 | // Created by Mehmet Ateş on 12.11.2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View { 12 | clipShape(RoundedCorner(radius: radius, corners: corners)) 13 | } 14 | } 15 | 16 | struct RoundedCorner: Shape { 17 | var radius: CGFloat = .infinity 18 | var corners: UIRectCorner = .allCorners 19 | func path(in rect: CGRect) -> Path { 20 | let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) 21 | return Path(path.cgPath) 22 | } 23 | } 24 | 25 | public enum Themes: String, CaseIterable { 26 | case currency = "Currency" 27 | case light = "Light" 28 | case dark = "Dark" 29 | case love = "Love" 30 | case ocean = "Ocean" 31 | case natural = "Natural" 32 | case colorful = "Colorful" 33 | case sunset = "Sunset" 34 | case neon = "Neon" 35 | } 36 | 37 | public struct ColorThemes { 38 | public static let currency: [Color] = [ 39 | .init(hex: "007f5f"), 40 | .init(hex: "2b9348"), 41 | .init(hex: "55a630"), 42 | .init(hex: "80b918"), 43 | .init(hex: "00a3cc"), 44 | .init(hex: "0081a7"), 45 | .init(hex: "005082"), 46 | .init(hex: "993366"), 47 | .init(hex: "cc3366") 48 | ] 49 | 50 | public static let light: [Color] = [ 51 | .init(hex: "fec5bb"), 52 | .init(hex: "fcd5ce"), 53 | .init(hex: "fae1dd"), 54 | .init(hex: "f8edeb"), 55 | .init(hex: "e8e8e4"), 56 | .init(hex: "d8e2dc"), 57 | .init(hex: "ece4db"), 58 | .init(hex: "ffd7ba"), 59 | .init(hex: "fec89a") 60 | ] 61 | 62 | public static let dark: [Color] = [ 63 | .init(hex: "181818"), 64 | .init(hex: "282828"), 65 | .init(hex: "404048"), 66 | .init(hex: "505860"), 67 | .init(hex: "66707a"), 68 | .init(hex: "381820"), 69 | .init(hex: "501820"), 70 | .init(hex: "502028") 71 | ] 72 | 73 | public static let love: [Color] = [ 74 | .init(hex: "fff0f3"), 75 | .init(hex: "ffccd5"), 76 | .init(hex: "ffb3c1"), 77 | .init(hex: "ff8fa3"), 78 | .init(hex: "ff758f"), 79 | .init(hex: "ff4d6d"), 80 | .init(hex: "a4133c"), 81 | .init(hex: "800f2f"), 82 | .init(hex: "590d22") 83 | ] 84 | 85 | public static let ocean: [Color] = [ 86 | .init(hex: "a9d6e5"), 87 | .init(hex: "89c2d9"), 88 | .init(hex: "61a5c2"), 89 | .init(hex: "468faf"), 90 | .init(hex: "2c7da0"), 91 | .init(hex: "2a6f97"), 92 | .init(hex: "014f86"), 93 | .init(hex: "01497c"), 94 | .init(hex: "013a63"), 95 | .init(hex: "012a4a") 96 | ] 97 | 98 | public static let natural: [Color] = [ 99 | .init(hex: "d8f3dc"), 100 | .init(hex: "b7e4c7"), 101 | .init(hex: "95d5b2"), 102 | .init(hex: "74c69d"), 103 | .init(hex: "52b788"), 104 | .init(hex: "40916c"), 105 | .init(hex: "2d6a4f"), 106 | .init(hex: "1b4332"), 107 | .init(hex: "081c15") 108 | ] 109 | 110 | public static let colorful: [Color] = [ 111 | .init(hex: "ffadad"), 112 | .init(hex: "ffd6a5"), 113 | .init(hex: "fdffb6"), 114 | .init(hex: "caffbf"), 115 | .init(hex: "9bf6ff"), 116 | .init(hex: "a0c4ff"), 117 | .init(hex: "bdb2ff"), 118 | .init(hex: "ffc6ff"), 119 | .init(hex: "fffffc") 120 | ] 121 | 122 | public static let sunset: [Color] = [ 123 | .init(hex: "ff7b00"), 124 | .init(hex: "ff8800"), 125 | .init(hex: "ff9500"), 126 | .init(hex: "ffa200"), 127 | .init(hex: "ffaa00"), 128 | .init(hex: "ffb700"), 129 | .init(hex: "ffd000"), 130 | .init(hex: "ffea00"), 131 | ] 132 | 133 | public static let neon: [Color] = [ 134 | .init(hex: "f72585"), 135 | .init(hex: "b5179e"), 136 | .init(hex: "7209b7"), 137 | .init(hex: "560bad"), 138 | .init(hex: "3a0ca3"), 139 | .init(hex: "3f37c9"), 140 | .init(hex: "4361ee"), 141 | .init(hex: "4895ef"), 142 | .init(hex: "4cc9f0") 143 | ] 144 | 145 | public static func getColors(by theme: Themes) -> [Color] { 146 | switch theme { 147 | case .currency: 148 | ColorThemes.currency 149 | case .light: 150 | ColorThemes.light 151 | case .dark: 152 | ColorThemes.dark 153 | case .love: 154 | ColorThemes.love 155 | case .ocean: 156 | ColorThemes.ocean 157 | case .natural: 158 | ColorThemes.natural 159 | case .colorful: 160 | ColorThemes.colorful 161 | case .sunset: 162 | ColorThemes.sunset 163 | case .neon: 164 | ColorThemes.neon 165 | } 166 | } 167 | } 168 | 169 | public extension Color { 170 | static func getColor(_ colorTheme: [Color], _ index: Int) -> Color { 171 | if colorTheme.indices.contains(index) { 172 | return colorTheme[index] 173 | } else { 174 | return colorTheme[safe: index % colorTheme.count] ?? .primary 175 | } 176 | } 177 | 178 | static func themeColor(by index: Int, with theme: Themes) -> Color { 179 | switch theme { 180 | case .light: 181 | return getColor(ColorThemes.light, index) 182 | case .dark: 183 | return getColor(ColorThemes.dark, index) 184 | case .love: 185 | return getColor(ColorThemes.love, index) 186 | case .ocean: 187 | return getColor(ColorThemes.ocean, index) 188 | case .natural: 189 | return getColor(ColorThemes.natural, index) 190 | case .colorful: 191 | return getColor(ColorThemes.colorful, index) 192 | case .sunset: 193 | return getColor(ColorThemes.sunset, index) 194 | case .neon: 195 | return getColor(ColorThemes.neon, index) 196 | case .currency: 197 | return getColor(ColorThemes.currency, index) 198 | } 199 | } 200 | 201 | init(hex: String) { 202 | let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) 203 | var int: UInt64 = 0 204 | Scanner(string: hex).scanHexInt64(&int) 205 | let a, r, g, b: UInt64 206 | switch hex.count { 207 | case 3: // RGB (12-bit) 208 | (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) 209 | case 6: // RGB (24-bit) 210 | (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) 211 | case 8: // ARGB (32-bit) 212 | (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) 213 | default: 214 | (a, r, g, b) = (1, 1, 1, 0) 215 | } 216 | 217 | self.init( 218 | .sRGB, 219 | red: Double(r) / 255, 220 | green: Double(g) / 255, 221 | blue: Double(b) / 255, 222 | opacity: Double(a) / 255 223 | ) 224 | } 225 | } 226 | 227 | extension Collection { 228 | subscript(safe index: Index) -> Element? { 229 | return indices.contains(index) ? self[index] : nil 230 | } 231 | } 232 | --------------------------------------------------------------------------------