From ac7680320f8afc07afa82cdb16308483179e5e91 Mon Sep 17 00:00:00 2001 From: Andras Samu Date: Sun, 4 Apr 2021 15:54:15 +0200 Subject: [PATCH] feat(core): refactoring chart dispalying now it is possible to add background lines precisely as charts are displayed at correct size also rewrote basics to conform with Shapes and Animatable protocol --- .../UserInterfaceState.xcuserstate | Bin 0 -> 37983 bytes .../xcdebugger/Breakpoints_v2.xcbkptlist | 6 + .../xcschemes/xcschememanagement.plist | 15 +- .../SwiftUICharts/Base/Chart/ChartData.swift | 16 +- .../Base/Extensions/CGPoint+Extension.swift | 2 +- .../SwiftUICharts/Base/Grid/ChartGrid.swift | 43 ++++- .../Charts/BarChart/BarChartCell.swift | 41 ++-- .../Charts/BarChart/BarChartCellShape.swift | 44 +++++ .../Charts/BarChart/BarChartRow.swift | 31 ++-- .../SwiftUICharts/Charts/LineChart/Line.swift | 175 +++++------------- .../LineChart/LineBackgroundShape.swift | 31 ++++ .../LineChart/LineBackgroundShapeView.swift | 19 ++ .../Charts/LineChart/LineShape.swift | 30 +++ .../Charts/LineChart/LineShapeView.swift | 26 +++ .../ArrayExtensionTests.swift | 7 - 15 files changed, 292 insertions(+), 194 deletions(-) create mode 100644 .swiftpm/xcode/package.xcworkspace/xcuserdata/samuandris.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 .swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist create mode 100644 Sources/SwiftUICharts/Charts/BarChart/BarChartCellShape.swift create mode 100644 Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShape.swift create mode 100644 Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShapeView.swift create mode 100644 Sources/SwiftUICharts/Charts/LineChart/LineShape.swift create mode 100644 Sources/SwiftUICharts/Charts/LineChart/LineShapeView.swift diff --git a/.swiftpm/xcode/package.xcworkspace/xcuserdata/samuandris.xcuserdatad/UserInterfaceState.xcuserstate b/.swiftpm/xcode/package.xcworkspace/xcuserdata/samuandris.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..a5f64f0a3fb188a684c45c82893ce8c09350485d GIT binary patch literal 37983 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 Y3qKB5{5$+b$e;hZ{oeoj{|vYN15fB^BLDyZ literal 0 HcmV?d00001 diff --git a/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 00000000..867e4fe8 --- /dev/null +++ b/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcschemes/xcschememanagement.plist b/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcschemes/xcschememanagement.plist index a0f26bbb..1be8e537 100644 --- a/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/.swiftpm/xcode/xcuserdata/samuandris.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,20 @@ SwiftUICharts.xcscheme_^#shared#^_ orderHint - 2 + 0 + + + SuppressBuildableAutocreation + + SwiftUICharts + + primary + + + SwiftUIChartsTests + + primary + diff --git a/Sources/SwiftUICharts/Base/Chart/ChartData.swift b/Sources/SwiftUICharts/Base/Chart/ChartData.swift index 28c2065d..b598975c 100644 --- a/Sources/SwiftUICharts/Base/Chart/ChartData.swift +++ b/Sources/SwiftUICharts/Base/Chart/ChartData.swift @@ -12,8 +12,20 @@ public class ChartData: ObservableObject { data.map { $0.0 } } - /// Initialize with data array - /// - Parameter data: Array of `Double` + var normalisedPoints: [Double] { + points.map { $0 / (points.max() ?? 1.0) } + } + + var normalisedRange: Double { + (normalisedPoints.max() ?? 0.0) - (normalisedPoints.min() ?? 0.0) + } + + var isInNegativeDomain: Bool { + (points.min() ?? 0.0) < 0 + } + + /// Initialize with data array + /// - Parameter data: Array of `Double` public init(_ data: [Double]) { self.data = data.map { ("", $0) } } diff --git a/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift index a9a9b086..363cd2c3 100644 --- a/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/CGPoint+Extension.swift @@ -8,7 +8,7 @@ extension CGPoint { /// - data: array of `Double` /// - Returns: X and Y delta as a `CGPoint` static func getStep(frame: CGRect, data: [Double]) -> CGPoint { - let padding: CGFloat = 30.0 + let padding: CGFloat = 0 // stepWidth var stepWidth: CGFloat = 0.0 diff --git a/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift b/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift index 4562b37f..ecaf270b 100644 --- a/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift +++ b/Sources/SwiftUICharts/Base/Grid/ChartGrid.swift @@ -1,25 +1,52 @@ import SwiftUI -/// <#Description#> public struct ChartGrid: View, ChartBase { public var chartData = ChartData() let content: () -> Content + let numberOfHorizontalLines = 4 @EnvironmentObject var data: ChartData @EnvironmentObject var style: ChartStyle - /// <#Description#> - /// - Parameter content: <#content description#> public init(@ViewBuilder content: @escaping () -> Content) { self.content = content } - /// The content and behavior of the `ChartGrid`. - /// - /// TODO: Explain why this is in a `ZStack` public var body: some View { - ZStack{ - self.content() + HStack { + ZStack { + VStack { + ForEach(0.. Path { + let baseLine: CGFloat = CGFloat(frame.height / 2) + var hLine = Path() + hLine.move(to: CGPoint(x:0, y: baseLine)) + hLine.addLine(to: CGPoint(x: frame.width, y: baseLine)) + return hLine + } + + var body: some View { + GeometryReader { geometry in + line(frame: geometry.frame(in: .local)) + .stroke(Color(white: 0.3), style: StrokeStyle(lineWidth: 1, lineCap: .round, dash: [5, 10])) } } } diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift index 761add15..e457d3cd 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartCell.swift @@ -4,27 +4,17 @@ import SwiftUI public struct BarChartCell: View { var value: Double var index: Int = 0 - var width: Float - var numberOfDataPoints: Int var gradientColor: ColorGradient var touchLocation: CGFloat - var cellWidth: Double { - return Double(width)/(Double(numberOfDataPoints) * 1.5) - } - - @State private var firstDisplay: Bool = true + @State private var didCellAppear: Bool = false public init( value: Double, index: Int = 0, - width: Float, - numberOfDataPoints: Int, gradientColor: ColorGradient, touchLocation: CGFloat) { self.value = value self.index = index - self.width = width - self.numberOfDataPoints = numberOfDataPoints self.gradientColor = gradientColor self.touchLocation = touchLocation } @@ -33,20 +23,15 @@ public struct BarChartCell: View { /// /// Animated when first displayed, using the `firstDisplay` variable, with an increasing delay through the data set. public var body: some View { - ZStack { - RoundedRectangle(cornerRadius: 4) - .fill(gradientColor.linearGradient(from: .bottom, to: .top)) - } - .frame(width: CGFloat(self.cellWidth)) - .scaleEffect(CGSize(width: 1, height: self.firstDisplay ? 0.0 : self.value), anchor: .bottom) - .onAppear { - self.firstDisplay = false + BarChartCellShape(value: didCellAppear ? value : 0.0) + .fill(gradientColor.linearGradient(from: .bottom, to: .top)) .onAppear { + self.didCellAppear = true } .onDisappear { - self.firstDisplay = true + self.didCellAppear = false } .transition(.slide) - .animation(Animation.spring().delay(self.touchLocation < 0 || !firstDisplay ? Double(self.index) * 0.04 : 0)) + .animation(Animation.spring().delay(self.touchLocation < 0 || !didCellAppear ? Double(self.index) * 0.04 : 0)) } } @@ -54,17 +39,17 @@ struct BarChartCell_Previews: PreviewProvider { static var previews: some View { Group { Group { - BarChartCell(value: 0, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat()) + BarChartCell(value: 0, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat()) - BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat()) - BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat()) - BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat()) + BarChartCell(value: 0.5, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat()) + BarChartCell(value: 0.75, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat()) + BarChartCell(value: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat()) } Group { - BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat()) - BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat()) - BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat()) + BarChartCell(value: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat()) + BarChartCell(value: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat()) + BarChartCell(value: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat()) }.environment(\.colorScheme, .dark) } } diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartCellShape.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartCellShape.swift new file mode 100644 index 00000000..5cf99839 --- /dev/null +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartCellShape.swift @@ -0,0 +1,44 @@ +import SwiftUI + +struct BarChartCellShape: Shape, Animatable { + var value: Double + var cornerRadius: CGFloat = 6.0 + var animatableData: CGFloat { + get { CGFloat(value) } + set { value = Double(newValue) } + } + + func path(in rect: CGRect) -> Path { + let adjustedOriginY = rect.height - (rect.height * CGFloat(value)) + var path = Path() + path.move(to: CGPoint(x: 0.0 , y: rect.height)) + path.addLine(to: CGPoint(x: 0.0, y: adjustedOriginY + cornerRadius)) + path.addArc(center: CGPoint(x: cornerRadius, y: adjustedOriginY + cornerRadius), + radius: cornerRadius, + startAngle: Angle(radians: Double.pi), + endAngle: Angle(radians: -Double.pi/2), + clockwise: false) + path.addLine(to: CGPoint(x: rect.width - cornerRadius, y: adjustedOriginY)) + path.addArc(center: CGPoint(x: rect.width - cornerRadius, y: adjustedOriginY + cornerRadius), + radius: cornerRadius, + startAngle: Angle(radians: -Double.pi/2), + endAngle: Angle(radians: 0), + clockwise: false) + path.addLine(to: CGPoint(x: rect.width, y: rect.height)) + path.closeSubpath() + + return path + } +} + +struct BarChartCellShape_Previews: PreviewProvider { + static var previews: some View { + Group { + BarChartCellShape(value: 0.75) + .fill(Color.red) + + BarChartCellShape(value: 0.3) + .fill(Color.blue) + } + } +} diff --git a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift index 366a3319..5cfd64b9 100644 --- a/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift +++ b/Sources/SwiftUICharts/Charts/BarChart/BarChartRow.swift @@ -6,10 +6,6 @@ public struct BarChartRow: View { @ObservedObject var chartData: ChartData @State private var touchLocation: CGFloat = -1.0 - enum Constant { - static let spacing: CGFloat = 16.0 - } - var style: ChartStyle var maxValue: Double { @@ -27,20 +23,18 @@ public struct BarChartRow: View { public var body: some View { GeometryReader { geometry in HStack(alignment: .bottom, - spacing: (geometry.frame(in: .local).width - Constant.spacing) / CGFloat(self.chartData.data.count * 3)) { - ForEach(0.. Double { - return Double(chartData.points[index])/Double(maxValue) - } - /// Size to scale the touch indicator /// - Parameters: /// - touchLocation: fraction of width where touch is happening @@ -87,3 +74,11 @@ public struct BarChartRow: View { return self.chartData.points[index] } } + +struct BarChartRow_Previews: PreviewProvider { + static let chartData = ChartData([6, 2, 5, 8, 6]) + static let chartStyle = ChartStyle(backgroundColor: .white, foregroundColor: .orangeBright) + static var previews: some View { + BarChartRow(chartData: chartData, style: chartStyle) + } +} diff --git a/Sources/SwiftUICharts/Charts/LineChart/Line.swift b/Sources/SwiftUICharts/Charts/LineChart/Line.swift index 942342de..0e9fa34f 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/Line.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/Line.swift @@ -3,57 +3,16 @@ import SwiftUI /// A single line of data, a view in a `LineChart` public struct Line: View { @EnvironmentObject var chartValue: ChartValue - @State private var frame: CGRect = .zero @ObservedObject var chartData: ChartData var style: ChartStyle @State private var showIndicator: Bool = false @State private var touchLocation: CGPoint = .zero - @State private var showFull: Bool = false @State private var showBackground: Bool = true - var curvedLines: Bool = true - - /// Step for plotting through data - /// - Returns: X and Y delta between each data point based on data and view's frame - var step: CGPoint { - return CGPoint.getStep(frame: frame, data: chartData.points) - } - - /// Path of line graph - /// - Returns: A path for stroking representing the data, either curved or jagged. - var path: Path { - let points = chartData.points - - if curvedLines { - return Path.quadCurvedPathWithPoints(points: points, - step: step, - globalOffset: nil) - } - - return Path.linePathWithPoints(points: points, step: step) - } - - /// Path of linegraph, but also closed at the bottom side - /// - Returns: A path for filling representing the data, either curved or jagged - var closedPath: Path { - let points = chartData.points - - if curvedLines { - return Path.quadClosedCurvedPathWithPoints(points: points, - step: step, - globalOffset: nil) - } - - return Path.closedLinePathWithPoints(points: points, step: step) - } + @State private var didCellAppear: Bool = false - // see https://stackoverflow.com/a/62370919 - // This lets geometry be recalculated when device rotates. However it doesn't cover issue of app changing - // from full screen to split view. Not possible in SwiftUI? Feedback submitted to apple FB8451194. - let orientationChanged = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification) - .makeConnectable() - .autoconnect() + var curvedLines: Bool = true /// The content and behavior of the `Line`. /// Draw the background if showing the full line (?) and the `showBackground` option is set. Above that draw the line, and then the data indicator if the graph is currently being touched. @@ -62,34 +21,35 @@ public struct Line: View { public var body: some View { GeometryReader { geometry in ZStack { - if self.showFull && self.showBackground { - self.getBackgroundPathView() - } - self.getLinePathView() - if self.showIndicator { - IndicatorPoint() - .position(self.getClosestPointOnPath(touchLocation: self.touchLocation)) - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + if self.didCellAppear && self.showBackground { + LineBackgroundShapeView(chartData: chartData, + geometry: geometry, + style: style) } + LineShapeView(chartData: chartData, + geometry: geometry, + style: style, + trimTo: didCellAppear ? 1.0 : 0.0) + .animation(.easeIn) +// if self.showIndicator { +// IndicatorPoint() +// .position(self.getClosestPointOnPath(touchLocation: self.touchLocation)) +// .rotationEffect(.degrees(180), anchor: .center) +// .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) +// } } .onAppear { - self.frame = geometry.frame(in: .local) - + didCellAppear = true + } + .onDisappear() { + didCellAppear = false } - .onReceive(orientationChanged) { _ in - // When we receive notification here, the geometry is still the old value - // so delay evaluation to get the new frame! - DispatchQueue.main.async { - self.frame = geometry.frame(in: .local) // recalculate layout with new frame - } - } .gesture(DragGesture() .onChanged({ value in self.touchLocation = value.location self.showIndicator = true - self.getClosestDataPoint(point: self.getClosestPointOnPath(touchLocation: value.location)) +// self.getClosestDataPoint(point: self.getClosestPointOnPath(touchLocation: value.location)) self.chartValue.interactionInProgress = true }) .onEnded({ value in @@ -104,80 +64,37 @@ public struct Line: View { // MARK: - Private functions -extension Line { - - /// Calculate point closest to where the user touched - /// - Parameter touchLocation: location in view where touched - /// - Returns: `CGPoint` of data point on chart - private func getClosestPointOnPath(touchLocation: CGPoint) -> CGPoint { - let closest = self.path.point(to: touchLocation.x) - return closest - } +//extension Line { +// /// Calculate point closest to where the user touched +// /// - Parameter touchLocation: location in view where touched +// /// - Returns: `CGPoint` of data point on chart +// private func getClosestPointOnPath(touchLocation: CGPoint) -> CGPoint { +// let closest = self.path.point(to: touchLocation.x) +// return closest +// } +// +// /// Figure out where closest touch point was +// /// - Parameter point: location of data point on graph, near touch location +// private func getClosestDataPoint(point: CGPoint) { +// let index = Int(round((point.x)/step.x)) +// if (index >= 0 && index < self.chartData.data.count){ +// self.chartValue.currentValue = self.chartData.points[index] +// } +// } +//} - /// Figure out where closest touch point was - /// - Parameter point: location of data point on graph, near touch location - private func getClosestDataPoint(point: CGPoint) { - let index = Int(round((point.x)/step.x)) - if (index >= 0 && index < self.chartData.data.count){ - self.chartValue.currentValue = self.chartData.points[index] - } - } +struct Line_Previews: PreviewProvider { + /// Predefined style, black over white, for preview + static let blackLineStyle = ChartStyle(backgroundColor: ColorGradient(.white), foregroundColor: ColorGradient(.black)) - /// Get the view representing the filled in background below the chart, filled with the foreground color's gradient - /// - /// TODO: explain rotations - /// - Returns: SwiftUI `View` - private func getBackgroundPathView() -> some View { - self.closedPath - .fill(LinearGradient(gradient: Gradient(colors: [ - style.foregroundColor.first?.startColor ?? .white, - style.foregroundColor.first?.endColor ?? .white, - .clear]), - startPoint: .bottom, - endPoint: .top)) - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) - .opacity(0.2) - .transition(.opacity) - .animation(.easeIn(duration: 1.6)) - } + /// Predefined style red over white, for preview + static let redLineStyle = ChartStyle(backgroundColor: .whiteBlack, foregroundColor: ColorGradient(.red)) - /// Get the view representing the line stroked in the `foregroundColor` - /// - /// TODO: Explain how `showFull` works - /// TODO: explain rotations - /// - Returns: SwiftUI `View` - private func getLinePathView() -> some View { - self.path - .trim(from: 0, to: self.showFull ? 1:0) - .stroke(LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient, - startPoint: .leading, - endPoint: .trailing), - style: StrokeStyle(lineWidth: 3, lineJoin: .round)) - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) - .animation(Animation.easeOut(duration: 1.2)) - .onAppear { - self.showFull = true - } - .onDisappear { - self.showFull = false - } - .drawingGroup() - } -} - -struct Line_Previews: PreviewProvider { static var previews: some View { Group { - Line(chartData: ChartData([8, 23, 32, 7, 23, 43]), style: blackLineStyle) + Line(chartData: ChartData([8, 23, 32, 7, 23, -4]), style: blackLineStyle) Line(chartData: ChartData([8, 23, 32, 7, 23, 43]), style: redLineStyle) } } } -/// Predefined style, black over white, for preview -private let blackLineStyle = ChartStyle(backgroundColor: ColorGradient(.white), foregroundColor: ColorGradient(.black)) - -/// Predefined stylem red over white, for preview -private let redLineStyle = ChartStyle(backgroundColor: .whiteBlack, foregroundColor: ColorGradient(.red)) diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShape.swift b/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShape.swift new file mode 100644 index 00000000..06b02811 --- /dev/null +++ b/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShape.swift @@ -0,0 +1,31 @@ +import SwiftUI + +struct LineBackgroundShape: Shape { + var data: [Double] + func path(in rect: CGRect) -> Path { + let path = Path.quadClosedCurvedPathWithPoints(points: data, step: CGPoint(x: 1.0, y: 1.0)) + return path + } +} + +struct LineBackgroundShape_Previews: PreviewProvider { + static var previews: some View { + Group { + GeometryReader { geometry in + LineBackgroundShape(data: [0, 0.5, 0.8, 0.6, 1]) + .transform(CGAffineTransform(scaleX: geometry.size.width / 4.0, y: geometry.size.height)) + .fill(Color.red) + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + } + GeometryReader { geometry in + LineBackgroundShape(data: [0, -0.5, 0.8, -0.6, 1]) + .transform(CGAffineTransform(scaleX: geometry.size.width / 4.0, y: geometry.size.height / 1.6)) + .fill(Color.blue) + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + } + } + } +} + diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShapeView.swift b/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShapeView.swift new file mode 100644 index 00000000..25ba8214 --- /dev/null +++ b/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShapeView.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct LineBackgroundShapeView: View { + var chartData: ChartData + var geometry: GeometryProxy + var style: ChartStyle + + var body: some View { + LineBackgroundShape(data: chartData.normalisedPoints) + .transform(CGAffineTransform(scaleX: geometry.size.width / CGFloat(chartData.normalisedPoints.count - 1), + y: geometry.size.height / CGFloat(chartData.normalisedRange))) + .fill(LinearGradient(gradient: Gradient(colors: [style.foregroundColor.first?.startColor ?? .white, + style.backgroundColor.startColor]), + startPoint: .bottom, + endPoint: .top)) + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + } +} diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineShape.swift b/Sources/SwiftUICharts/Charts/LineChart/LineShape.swift new file mode 100644 index 00000000..64fff658 --- /dev/null +++ b/Sources/SwiftUICharts/Charts/LineChart/LineShape.swift @@ -0,0 +1,30 @@ +import SwiftUI + +struct LineShape: Shape { + var data: [Double] + func path(in rect: CGRect) -> Path { + let path = Path.quadCurvedPathWithPoints(points: data, step: CGPoint(x: 1.0, y: 1.0)) + return path + } +} + +struct LineShape_Previews: PreviewProvider { + static var previews: some View { + Group { + GeometryReader { geometry in + LineShape(data: [0, 0.5, 0.8, 0.6, 1]) + .transform(CGAffineTransform(scaleX: geometry.size.width / 4.0, y: geometry.size.height)) + .stroke(Color.red) + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + } + GeometryReader { geometry in + LineShape(data: [0, -0.5, 0.8, -0.6, 1]) + .transform(CGAffineTransform(scaleX: geometry.size.width / 4.0, y: geometry.size.height / 1.6)) + .stroke(Color.blue) + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + } + } + } +} diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineShapeView.swift b/Sources/SwiftUICharts/Charts/LineChart/LineShapeView.swift new file mode 100644 index 00000000..d7143932 --- /dev/null +++ b/Sources/SwiftUICharts/Charts/LineChart/LineShapeView.swift @@ -0,0 +1,26 @@ +import SwiftUI + +struct LineShapeView: View, Animatable { + var chartData: ChartData + var geometry: GeometryProxy + var style: ChartStyle + var trimTo: Double = 0 + + var animatableData: CGFloat { + get { CGFloat(trimTo) } + set { trimTo = Double(newValue) } + } + + var body: some View { + LineShape(data: chartData.normalisedPoints) + .trim(from: 0, to: CGFloat(trimTo)) + .transform(CGAffineTransform(scaleX: geometry.size.width / CGFloat(chartData.normalisedPoints.count - 1), + y: geometry.size.height / CGFloat(chartData.normalisedRange))) + .stroke(LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient, + startPoint: .leading, + endPoint: .trailing), + style: StrokeStyle(lineWidth: 3, lineJoin: .round)) + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + } +} diff --git a/Tests/SwiftUIChartsTests/ArrayExtensionTests.swift b/Tests/SwiftUIChartsTests/ArrayExtensionTests.swift index ad77114c..f76c00e2 100644 --- a/Tests/SwiftUIChartsTests/ArrayExtensionTests.swift +++ b/Tests/SwiftUIChartsTests/ArrayExtensionTests.swift @@ -1,10 +1,3 @@ -// -// File.swift -// -// -// Created by Nicolas Savoini on 2020-05-25. -// - @testable import SwiftUICharts import XCTest