diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 00000000..60e30109 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,64 @@ +disabled_rules: +- explicit_acl +- trailing_whitespace +- force_cast +- unused_closure_parameter +- multiple_closures_with_trailing_closure +opt_in_rules: +- anyobject_protocol +- array_init +- attributes +- collection_alignment +- colon +- conditional_returns_on_newline +- convenience_type +- empty_count +- empty_string +- empty_collection_literal +- enum_case_associated_values_count +- function_default_parameter_at_end +- fatal_error_message +- file_name +- first_where +- modifier_order +- toggle_bool +- unused_private_declaration +- yoda_condition +excluded: +- Carthage +- Pods +- SwiftLint/Common/3rdPartyLib +identifier_name: + excluded: + - a + - b + - c + - i + - id + - t + - to + - x + - y +line_length: + warning: 150 + error: 200 + ignores_function_declarations: true + ignores_comments: true + ignores_urls: true +function_body_length: + warning: 300 + error: 500 +function_parameter_count: + warning: 6 + error: 8 +type_body_length: + warning: 300 + error: 400 +file_length: + warning: 500 + error: 1200 + ignore_comment_only_lines: true +cyclomatic_complexity: + warning: 15 + error: 21 +reporter: "xcode" diff --git a/Package.swift b/Package.swift index ffd10e06..5a466752 100644 --- a/Package.swift +++ b/Package.swift @@ -6,13 +6,13 @@ import PackageDescription let package = Package( name: "SwiftUICharts", platforms: [ - .iOS(.v13),.watchOS(.v6) + .iOS(.v13), .watchOS(.v6) ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( name: "SwiftUICharts", - targets: ["SwiftUICharts"]), + targets: ["SwiftUICharts"]) ], dependencies: [ // Dependencies declare other packages that this package depends on. @@ -26,6 +26,6 @@ let package = Package( dependencies: []), .testTarget( name: "SwiftUIChartsTests", - dependencies: ["SwiftUICharts"]), + dependencies: ["SwiftUICharts"]) ] ) diff --git a/Sources/SwiftUICharts/Base/Extensions/CGRect+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/CGRect+Extension.swift new file mode 100644 index 00000000..c84d8374 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Extensions/CGRect+Extension.swift @@ -0,0 +1,16 @@ +// +// CGRect+Extension.swift +// SwiftUICharts +// +// Created by Nicolas Savoini on 2020-05-24. +// + +import Foundation +import SwiftUI + +extension CGRect { + // Return the coordinate for a rectangle center + public var mid: CGPoint { + return CGPoint(x: self.midX, y: self.midY) + } +} diff --git a/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift index e8b24983..a018c3c9 100644 --- a/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/Color+Extension.swift @@ -5,17 +5,17 @@ extension Color { let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) var int = UInt64() Scanner(string: hex).scanHexInt64(&int) - let r, g, b: UInt64 + let red, green, blue: UInt64 switch hex.count { case 3: // RGB (12-bit) - (r, g, b) = ((int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + (red, green, blue) = ((int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) case 6: // RGB (24-bit) - (r, g, b) = (int >> 16, int >> 8 & 0xFF, int & 0xFF) + (red, green, blue) = (int >> 16, int >> 8 & 0xFF, int & 0xFF) case 8: // ARGB (32-bit) - (r, g, b) = (int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + (red, green, blue) = (int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) default: - (r, g, b) = (0, 0, 0) + (red, green, blue) = (0, 0, 0) } - self.init(red: Double(r) / 255, green: Double(g) / 255, blue: Double(b) / 255) + self.init(red: Double(red) / 255, green: Double(green) / 255, blue: Double(blue) / 255) } } diff --git a/Sources/SwiftUICharts/Base/Style/ChartStyle.swift b/Sources/SwiftUICharts/Base/Style/ChartStyle.swift index b7474f8c..e23ede3a 100644 --- a/Sources/SwiftUICharts/Base/Style/ChartStyle.swift +++ b/Sources/SwiftUICharts/Base/Style/ChartStyle.swift @@ -1,11 +1,28 @@ import SwiftUI public struct ChartStyle { - public let backgroundColor: Color - public let foregroundColor: ColorGradient + public let backgroundColor: ColorGradient + public let foregroundColor: [ColorGradient] + + public init(backgroundColor: Color, foregroundColor: [ColorGradient]) { + self.backgroundColor = ColorGradient.init(backgroundColor) + self.foregroundColor = foregroundColor + } + public init(backgroundColor: Color, foregroundColor: ColorGradient) { + self.backgroundColor = ColorGradient.init(backgroundColor) + self.foregroundColor = [foregroundColor] + } + + public init(backgroundColor: ColorGradient, foregroundColor: ColorGradient) { + self.backgroundColor = backgroundColor + self.foregroundColor = [foregroundColor] + } + + public init(backgroundColor: ColorGradient, foregroundColor: [ColorGradient]) { self.backgroundColor = backgroundColor self.foregroundColor = foregroundColor } + } diff --git a/Sources/SwiftUICharts/Base/Style/ColorGradient.swift b/Sources/SwiftUICharts/Base/Style/ColorGradient.swift index 18a4ab20..827e0096 100644 --- a/Sources/SwiftUICharts/Base/Style/ColorGradient.swift +++ b/Sources/SwiftUICharts/Base/Style/ColorGradient.swift @@ -4,6 +4,11 @@ public struct ColorGradient { public let startColor: Color public let endColor: Color + public init(_ color: Color) { + self.startColor = color + self.endColor = color + } + public init (_ startColor: Color, _ endColor: Color) { self.startColor = startColor self.endColor = endColor @@ -13,3 +18,21 @@ public struct ColorGradient { return Gradient(colors: [startColor, endColor]) } } + +extension ColorGradient { + /// Convenience method to return a LinearGradient from the ColorGradient + /// - Parameters: + /// - startPoint: starting point + /// - endPoint: ending point + /// - Returns: a Linear gradient + public func linearGradient(from startPoint: UnitPoint, to endPoint: UnitPoint) -> LinearGradient { + return LinearGradient(gradient: self.gradient, startPoint: startPoint, endPoint: endPoint) + } +} + +extension ColorGradient { + public static let orangeBright = ColorGradient(ChartColors.orangeBright) + public static let redBlack = ColorGradient(.red, .black) + public static let greenRed = ColorGradient(.green, .red) + public static let whiteBlack = ColorGradient(.white, .black) +} diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift index 41b2e594..d91d1db0 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChart.swift @@ -2,8 +2,26 @@ import SwiftUI public struct BarChart: ChartType { public func makeChart(configuration: Self.Configuration, style: Self.Style) -> some View { - BarChartRow(data: configuration.data, gradientColor: style.foregroundColor) + BarChartRow(data: configuration.data, style: style) } - public init() {} } + +struct BarChart_Previews: PreviewProvider { + static var previews: some View { + Group { + Group { + BarChart().makeChart( + configuration: .init(data: [1, 2, 3, 5, 1]), + style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack)) + }.environment(\.colorScheme, .light) + + Group { + BarChart().makeChart( + configuration: .init(data: [1, 2, 3]), + style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack)) + }.environment(\.colorScheme, .dark) + + } + } +} diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift index 1ccaa6df..22f2301b 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift @@ -1,6 +1,6 @@ import SwiftUI -public struct BarChartCell : View { +public struct BarChartCell: View { @State var value: Double @State var index: Int = 0 @State var width: Float @@ -13,16 +13,36 @@ public struct BarChartCell : View { @State var scaleValue: Double = 0 @Binding var touchLocation: CGFloat + public var body: some View { ZStack { RoundedRectangle(cornerRadius: 4) - .fill(LinearGradient(gradient: gradientColor.gradient, startPoint: .bottom, endPoint: .top)) + .fill(gradientColor.linearGradient(from: .bottom, to: .top)) } .frame(width: CGFloat(self.cellWidth)) .scaleEffect(CGSize(width: 1, height: self.scaleValue), anchor: .bottom) - .onAppear(){ + .onAppear { self.scaleValue = self.value } .animation(Animation.spring().delay(self.touchLocation < 0 ? Double(self.index) * 0.04 : 0)) } } + +struct BarChartCell_Previews: PreviewProvider { + static var previews: some View { + Group { + Group { + BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: .constant(CGFloat())) + BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: .constant(CGFloat())) + BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient(.purple), touchLocation: .constant(CGFloat())) + } + + Group { + BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: .constant(CGFloat())) + BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: .constant(CGFloat())) + BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient(.purple), touchLocation: .constant(CGFloat())) + }.environment(\.colorScheme, .dark) + } + + } +} diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift index 524f3621..4a517597 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift @@ -1,6 +1,6 @@ import SwiftUI -public struct BarChartRow : View { +public struct BarChartRow: View { @State var data: [Double] = [] @State var touchLocation: CGFloat = -1.0 @@ -8,7 +8,8 @@ public struct BarChartRow : View { static let spacing: CGFloat = 16.0 } - var gradientColor: ColorGradient + var style: ChartStyle + var maxValue: Double { data.max() ?? 0 } @@ -17,14 +18,14 @@ public struct BarChartRow : View { GeometryReader { geometry in HStack(alignment: .bottom, spacing: (geometry.frame(in: .local).width - Constant.spacing) / CGFloat(self.data.count * 3)) { - ForEach(0.. some View { self.closedPath - .fill(LinearGradient(gradient: gradientColor.gradient, startPoint: .bottom, endPoint: .top)) + .fill(style.backgroundColor.linearGradient(from: .bottom, to: .top)) .rotationEffect(.degrees(180), anchor: .center) .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) .transition(.opacity) @@ -89,7 +89,7 @@ extension Line { private func getLinePathView() -> some View { self.path .trim(from: 0, to: self.showFull ? 1:0) - .stroke(LinearGradient(gradient: gradientColor.gradient, + .stroke(LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient, startPoint: .leading, endPoint: .trailing), style: StrokeStyle(lineWidth: 3, lineJoin: .round)) @@ -105,3 +105,15 @@ extension Line { .drawingGroup() } } + +struct Line_Previews: PreviewProvider { + static var previews: some View { + Group { + Line(data: [1, 2, 3, 1, 2, 5, 7], style: blackLineStyle) + Line(data: [1, 2, 3, 1, 2, 5, 7], style: redLineStyle) + } + } +} + +private let blackLineStyle = ChartStyle(backgroundColor: ColorGradient(.white), foregroundColor: ColorGradient(.black)) +private let redLineStyle = ChartStyle(backgroundColor: .whiteBlack, foregroundColor: ColorGradient(.red)) diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift b/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift index cd3cb8c9..5bd76709 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/LineChart.swift @@ -2,8 +2,28 @@ import SwiftUI public struct LineChart: ChartType { public func makeChart(configuration: Self.Configuration, style: Self.Style) -> some View { - Line(data: configuration.data, gradientColor: style.foregroundColor) + Line(data: configuration.data, style: style) } public init() {} } + +struct LineChart_Previews: PreviewProvider { + static var previews: some View { + Group { + Group { + LineChart().makeChart( + configuration: .init(data: [1, 2, 3, 5, 1]), + style: .init(backgroundColor: .white, foregroundColor: ColorGradient(.black))) + }.environment(\.colorScheme, .light) + + Group { + LineChart().makeChart( + configuration: .init(data: [1, 2, 3]), + style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack)) + }.environment(\.colorScheme, .dark) + + } + + } +} diff --git a/Sources/SwiftUICharts/PieChart/PieChart.swift b/Sources/SwiftUICharts/PieChart/PieChart.swift new file mode 100644 index 00000000..9794fda8 --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/PieChart.swift @@ -0,0 +1,59 @@ +// +// PieChart.swift +// SwiftUICharts +// +// Created by Nicolas Savoini on 2020-05-24. +// + +import SwiftUI + +public struct PieChart: ChartType { + public func makeChart(configuration: Self.Configuration, style: Self.Style) -> some View { + PieChartRow(data: configuration.data, style: style) + } + public init() {} +} + +struct PieChart_Previews: PreviewProvider { + static var previews: some View { + Group { + Group { + PieChart().makeChart( + configuration: .init(data: [56, 78, 53, 65, 54]), + style: styleOneColor) + PieChart().makeChart( + configuration: .init(data: [56, 78, 53, 65, 54]), + style: styleTwoColor) + PieChart().makeChart( + configuration: .init(data: [1, 1, 1, 1, 1, 1]), + style: trivialPursuit) + }.environment(\.colorScheme, .light) + + Group { + PieChart().makeChart( + configuration: .init(data: [56, 78, 53, 65, 54]), + style: styleOneColor) + PieChart().makeChart( + configuration: .init(data: [56, 78, 53, 65, 54]), + style: styleTwoColor) + PieChart().makeChart( + configuration: .init(data: [1, 1, 1, 1, 1, 1]), + style: trivialPursuit) + }.environment(\.colorScheme, .dark) + + }.previewLayout(.fixed(width: 250, height: 400)) + } +} + +private let styleOneColor = ChartStyle(backgroundColor: .white, foregroundColor: ColorGradient.init(.pink)) + +private let styleTwoColor = ChartStyle(backgroundColor: ColorGradient(.black), foregroundColor: [ColorGradient(.yellow), ColorGradient(.red)]) + +private let trivialPursuit = ChartStyle( + backgroundColor: .yellow, + foregroundColor: [ColorGradient(.yellow), + ColorGradient(.pink), + ColorGradient(.green), + ColorGradient(.primary), + ColorGradient(.blue), + ColorGradient(.orange)]) diff --git a/Sources/SwiftUICharts/PieChart/PieChartCell.swift b/Sources/SwiftUICharts/PieChart/PieChartCell.swift new file mode 100644 index 00000000..20157677 --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/PieChartCell.swift @@ -0,0 +1,107 @@ +// +// PieChartCell.swift +// SwiftUICharts +// +// Created by Nicolas Savoini on 2020-05-24. +// + +import SwiftUI + +struct PieSlice: Identifiable { + var id = UUID() + var startDeg: Double + var endDeg: Double + var value: Double + //var normalizedValue: Double +} + +public struct PieChartCell: View { + @State private var show: Bool = false + var rect: CGRect + var radius: CGFloat { + return min(rect.width, rect.height)/2 + } + var startDeg: Double + var endDeg: Double + var path: Path { + var path = Path() + path.addArc( + center: rect.mid, + radius: self.radius, + startAngle: Angle(degrees: self.startDeg), + endAngle: Angle(degrees: self.endDeg), + clockwise: false) + path.addLine(to: rect.mid) + path.closeSubpath() + return path + } + var index: Int + + // Section line border color + var backgroundColor: Color + + // Section color + var accentColor: ColorGradient + + public var body: some View { + Group { + path + .fill(self.accentColor.linearGradient(from: .bottom, to: .top)) + .overlay(path.stroke(self.backgroundColor, lineWidth: 2)) + .scaleEffect(self.show ? 1 : 0) + .animation(Animation.spring().delay(Double(self.index) * 0.04)) + .onAppear { + self.show = true + } + + } + } +} + +struct PieChartCell_Previews: PreviewProvider { + static var previews: some View { + Group { + + GeometryReader { geometry in + PieChartCell( + rect: geometry.frame(in: .local), + startDeg: 00.0, + endDeg: 90.0, + index: 0, + backgroundColor: Color.red, + accentColor: ColorGradient.greenRed) + }.frame(width: 100, height: 100) + + GeometryReader { geometry in + PieChartCell( + rect: geometry.frame(in: .local), + startDeg: 0.0, + endDeg: 90.0, + index: 0, + backgroundColor: Color.green, + accentColor: ColorGradient.redBlack) + }.frame(width: 100, height: 100) + + GeometryReader { geometry in + PieChartCell( + rect: geometry.frame(in: .local), + startDeg: 100.0, + endDeg: 135.0, + index: 0, + backgroundColor: Color.black, + accentColor: ColorGradient.whiteBlack) + }.frame(width: 100, height: 100) + + GeometryReader { geometry in + PieChartCell( + rect: geometry.frame(in: .local), + startDeg: 185.0, + endDeg: 290.0, + index: 0, + backgroundColor: Color.purple, + accentColor: ColorGradient(.purple)) + }.frame(width: 100, height: 100) + + }.previewLayout(.fixed(width: 125, height: 125)) + } +} diff --git a/Sources/SwiftUICharts/PieChart/PieChartRow.swift b/Sources/SwiftUICharts/PieChart/PieChartRow.swift new file mode 100644 index 00000000..4915bd97 --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/PieChartRow.swift @@ -0,0 +1,66 @@ +// +// PieChartRow.swift +// SwiftUICharts +// +// Created by Nicolas Savoini on 2020-05-24. +// + +import SwiftUI + +public struct PieChartRow: View { + var data: [Double] + + var style: ChartStyle + + var slices: [PieSlice] { + var tempSlices: [PieSlice] = [] + var lastEndDeg: Double = 0 + let maxValue = data.reduce(0, +) + + for slice in data { + let normalized: Double = Double(slice)/Double(maxValue) + let startDeg = lastEndDeg + let endDeg = lastEndDeg + (normalized * 360) + lastEndDeg = endDeg + tempSlices.append(PieSlice(startDeg: startDeg, endDeg: endDeg, value: slice)) + } + + return tempSlices + } + + public var body: some View { + GeometryReader { geometry in + ZStack { + ForEach(0..