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

29 |

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 |
--------------------------------------------------------------------------------