diff --git a/Sources/SwiftUICharts/Base/Axis/AxisLabels.swift b/Sources/SwiftUICharts/Base/Axis/AxisLabels.swift index 94e1ffdc..0efaa897 100644 --- a/Sources/SwiftUICharts/Base/Axis/AxisLabels.swift +++ b/Sources/SwiftUICharts/Base/Axis/AxisLabels.swift @@ -1,40 +1,98 @@ import SwiftUI public struct AxisLabels: View { + struct YAxisViewKey: ViewPreferenceKey { } + struct ChartViewKey: ViewPreferenceKey { } + var axisLabelsData = AxisLabelsData() + var axisLabelsStyle = AxisLabelsStyle() + + @State private var yAxisWidth: CGFloat = 25 + @State private var chartWidth: CGFloat = 0 + @State private var chartHeight: CGFloat = 0 + let content: () -> Content - // font - // foregroundColor public init(@ViewBuilder content: @escaping () -> Content) { self.content = content } + var yAxis: some View { + VStack(spacing: 0.0) { + ForEach(Array(axisLabelsData.axisYLabels.reversed().enumerated()), id: \.element) { index, axisYData in + Text(axisYData) + .font(axisLabelsStyle.axisFont) + .foregroundColor(axisLabelsStyle.axisFontColor) + .frame(height: getYHeight(index: index, + chartHeight: chartHeight, + count: axisLabelsData.axisYLabels.count), + alignment: getYAlignment(index: index, count: axisLabelsData.axisYLabels.count)) + } + } + .padding([.leading, .trailing], 4.0) + .background(ViewGeometry()) + .onPreferenceChange(YAxisViewKey.self) { value in + yAxisWidth = value.first?.size.width ?? 0.0 + } + } + + func xAxis(chartWidth: CGFloat) -> some View { + HStack(spacing: 0.0) { + ForEach(Array(axisLabelsData.axisXLabels.enumerated()), id: \.element) { index, axisXData in + Text(axisXData) + .font(axisLabelsStyle.axisFont) + .foregroundColor(axisLabelsStyle.axisFontColor) + .frame(width: chartWidth / CGFloat(axisLabelsData.axisXLabels.count - 1)) + } + } + .frame(height: 24.0, alignment: .top) + } + + var chart: some View { + self.content() + .background(ViewGeometry()) + .onPreferenceChange(ChartViewKey.self) { value in + chartWidth = value.first?.size.width ?? 0.0 + chartHeight = value.first?.size.height ?? 0.0 + } + } + public var body: some View { - HStack { - VStack { - ForEach(Array(axisLabelsData.axisYLabels.reversed().enumerated()), id: \.element) { index, axisYData in - Text(axisYData) - if index != axisLabelsData.axisYLabels.count - 1 { - Spacer() - } + VStack(spacing: 0.0) { + HStack { + if axisLabelsStyle.axisLabelsYPosition == .leading { + yAxis + } else { + Spacer(minLength: yAxisWidth) } - } - .padding([.trailing], 8.0) - .padding([.bottom], axisLabelsData.axisXLabels.count > 0 ? 28.0 : 0) - .frame(maxHeight: .infinity) - VStack { - self.content() - HStack { - ForEach(Array(axisLabelsData.axisXLabels.enumerated()), id: \.element) { index, axisXData in - Text(axisXData) - if index != axisLabelsData.axisXLabels.count - 1 { - Spacer() - } - } + chart + if axisLabelsStyle.axisLabelsYPosition == .leading { + Spacer(minLength: yAxisWidth) + } else { + yAxis } } - .padding([.top, .bottom], 10.0) + xAxis(chartWidth: chartWidth) } } + + private func getYHeight(index: Int, chartHeight: CGFloat, count: Int) -> CGFloat { + if index == 0 || index == count - 1 { + return chartHeight / (CGFloat(count - 1) * 2) + 10 + } + + return chartHeight / CGFloat(count - 1) + } + + private func getYAlignment(index: Int, count: Int) -> Alignment { + if index == 0 { + return .top + } + + if index == count - 1 { + return .bottom + } + + return .center + } } diff --git a/Sources/SwiftUICharts/Base/Axis/Extension/AxisLabels+Extension.swift b/Sources/SwiftUICharts/Base/Axis/Extension/AxisLabels+Extension.swift index b7297d50..7698ac4e 100644 --- a/Sources/SwiftUICharts/Base/Axis/Extension/AxisLabels+Extension.swift +++ b/Sources/SwiftUICharts/Base/Axis/Extension/AxisLabels+Extension.swift @@ -1,8 +1,10 @@ import SwiftUI extension AxisLabels { - public func setAxisYLabels(_ labels: [String]) -> AxisLabels { + public func setAxisYLabels(_ labels: [String], + position: AxisLabelsYPosition = .leading) -> AxisLabels { self.axisLabelsData.axisYLabels = labels + self.axisLabelsStyle.axisLabelsYPosition = position return self } @@ -10,4 +12,46 @@ extension AxisLabels { self.axisLabelsData.axisXLabels = labels return self } + + public func setAxisYLabels(_ labels: [(Double, String)], + range: ClosedRange, + position: AxisLabelsYPosition = .leading) -> AxisLabels { + let overreach = range.overreach + 1 + var labelArray = [String](repeating: "", count: overreach) + labels.forEach { + let index = Int($0.0) - range.lowerBound + if labelArray[safe: index] != nil { + labelArray[index] = $0.1 + } + } + + self.axisLabelsData.axisYLabels = labelArray + self.axisLabelsStyle.axisLabelsYPosition = position + + return self + } + + public func setAxisXLabels(_ labels: [(Double, String)], range: ClosedRange) -> AxisLabels { + let overreach = range.overreach + 1 + var labelArray = [String](repeating: "", count: overreach) + labels.forEach { + let index = Int($0.0) - range.lowerBound + if labelArray[safe: index] != nil { + labelArray[index] = $0.1 + } + } + + self.axisLabelsData.axisXLabels = labelArray + return self + } + + public func setColor(_ color: Color) -> AxisLabels { + self.axisLabelsStyle.axisFontColor = color + return self + } + + public func setFont(_ font: Font) -> AxisLabels { + self.axisLabelsStyle.axisFont = font + return self + } } diff --git a/Sources/SwiftUICharts/Base/Axis/Model/AxisLabelsPosition.swift b/Sources/SwiftUICharts/Base/Axis/Model/AxisLabelsPosition.swift new file mode 100644 index 00000000..66735d7b --- /dev/null +++ b/Sources/SwiftUICharts/Base/Axis/Model/AxisLabelsPosition.swift @@ -0,0 +1,11 @@ +import Foundation + +public enum AxisLabelsYPosition { + case leading + case trailing +} + +public enum AxisLabelsXPosition { + case top + case bottom +} diff --git a/Sources/SwiftUICharts/Base/Axis/Model/AxisLabelsStyle.swift b/Sources/SwiftUICharts/Base/Axis/Model/AxisLabelsStyle.swift new file mode 100644 index 00000000..58221426 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Axis/Model/AxisLabelsStyle.swift @@ -0,0 +1,11 @@ +import SwiftUI + +public final class AxisLabelsStyle: ObservableObject { + @Published public var axisFont: Font = .callout + @Published public var axisFontColor: Color = .primary + @Published var axisLabelsYPosition: AxisLabelsYPosition = .leading + @Published var axisLabelsXPosition: AxisLabelsXPosition = .bottom + public init() { + // no-op + } +} diff --git a/Sources/SwiftUICharts/Base/Common/ViewGeometry.swift b/Sources/SwiftUICharts/Base/Common/ViewGeometry.swift new file mode 100644 index 00000000..ea8357f1 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Common/ViewGeometry.swift @@ -0,0 +1,10 @@ +import SwiftUI + +public struct ViewGeometry: View where T: PreferenceKey { + public var body: some View { + GeometryReader { geometry in + Color.clear + .preference(key: T.self, value: [ViewSizeData(size: geometry.size)] as! T.Value) + } + } +} diff --git a/Sources/SwiftUICharts/Base/Common/ViewPreferenceKey.swift b/Sources/SwiftUICharts/Base/Common/ViewPreferenceKey.swift new file mode 100644 index 00000000..d3c4c1f1 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Common/ViewPreferenceKey.swift @@ -0,0 +1,15 @@ +import SwiftUI + +public protocol ViewPreferenceKey: PreferenceKey { + typealias Value = [ViewSizeData] +} + +public extension ViewPreferenceKey { + static var defaultValue: [ViewSizeData] { + [] + } + + static func reduce(value: inout [ViewSizeData], nextValue: () -> [ViewSizeData]) { + value.append(contentsOf: nextValue()) + } +} diff --git a/Sources/SwiftUICharts/Base/Common/ViewSizeData.swift b/Sources/SwiftUICharts/Base/Common/ViewSizeData.swift new file mode 100644 index 00000000..9a53cec3 --- /dev/null +++ b/Sources/SwiftUICharts/Base/Common/ViewSizeData.swift @@ -0,0 +1,14 @@ +import SwiftUI + +public struct ViewSizeData: Identifiable, Equatable, Hashable { + public let id: UUID = UUID() + public let size: CGSize + + public static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift b/Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift index 874430ca..1e4bedd7 100644 --- a/Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift +++ b/Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift @@ -17,3 +17,10 @@ extension Array where Element == ColorGradient { return self[index] } } + +extension Collection { + /// Returns the element at the specified index if it is within bounds, otherwise nil. + subscript (safe index: Index) -> Element? { + return indices.contains(index) ? self[index] : nil + } +} diff --git a/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift b/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift index 4915ff39..432ce29c 100644 --- a/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift +++ b/Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift @@ -150,15 +150,17 @@ extension Path { static func drawChartMarkers(data: [(Double, Double)], in rect: CGRect) -> Path { var path = Path() - if data.count < 1 { + let filteredData = data.filter { $0.1 <= 1 && $0.1 >= 0 } + + if filteredData.count < 1 { return path } - let convertedXValues = data.map { CGFloat($0.0) * rect.width } - let convertedYPoints = data.map { CGFloat($0.1) * rect.height } + let convertedXValues = filteredData.map { CGFloat($0.0) * rect.width } + let convertedYPoints = filteredData.map { CGFloat($0.1) * rect.height } let markerSize = CGSize(width: 8, height: 8) - for pointIndex in 0.. LineChart { - self.chartProperties.showBackground = show + public func setBackground(colorGradient: ColorGradient) -> LineChart { + self.chartProperties.backgroundGradient = colorGradient return self } - public func showChartMarks(_ show: Bool) -> LineChart { + public func showChartMarks(_ show: Bool, with color: ColorGradient? = nil) -> LineChart { self.chartProperties.showChartMarks = show + self.chartProperties.customChartMarksColors = color return self } diff --git a/Sources/SwiftUICharts/Charts/LineChart/Line.swift b/Sources/SwiftUICharts/Charts/LineChart/Line.swift index cad3256b..4aa3394e 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/Line.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/Line.swift @@ -2,7 +2,6 @@ import SwiftUI /// A single line of data, a view in a `LineChart` public struct Line: View { - @EnvironmentObject var chartValue: ChartValue @ObservedObject var chartData: ChartData @ObservedObject var chartProperties: LineChartProperties @@ -29,17 +28,17 @@ public struct Line: View { public var body: some View { GeometryReader { geometry in ZStack { - if self.didCellAppear && self.chartProperties.showBackground { + if self.didCellAppear, let backgroundColor = chartProperties.backgroundGradient { LineBackgroundShapeView(chartData: chartData, geometry: geometry, - style: style) + backgroundColor: backgroundColor) } LineShapeView(chartData: chartData, chartProperties: chartProperties, geometry: geometry, style: style, trimTo: didCellAppear ? 1.0 : 0.0) - .animation(.easeIn) + .animation(Animation.easeIn(duration: 0.75)) if self.showIndicator { IndicatorPoint() .position(self.getClosestPointOnPath(geometry: geometry, @@ -54,20 +53,17 @@ public struct Line: View { .onDisappear() { didCellAppear = false } - - .gesture(DragGesture() - .onChanged({ value in - self.touchLocation = value.location - self.showIndicator = true - self.getClosestDataPoint(geometry: geometry, touchLocation: value.location) - self.chartValue.interactionInProgress = true - }) - .onEnded({ value in - self.touchLocation = .zero - self.showIndicator = false - self.chartValue.interactionInProgress = false - }) - ) +// .gesture(DragGesture() +// .onChanged({ value in +// self.touchLocation = value.location +// self.showIndicator = true +// self.getClosestDataPoint(geometry: geometry, touchLocation: value.location) +// }) +// .onEnded({ value in +// self.touchLocation = .zero +// self.showIndicator = false +// }) +// ) } } } @@ -94,7 +90,7 @@ extension Line { let geometryWidth = geometry.frame(in: .local).width let index = Int(round((touchLocation.x / geometryWidth) * CGFloat(chartData.points.count - 1))) if (index >= 0 && index < self.chartData.data.count){ - self.chartValue.currentValue = self.chartData.points[index] +// self.chartValue.currentValue = self.chartData.points[index] } } } diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShapeView.swift b/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShapeView.swift index 3251e960..4e7240cf 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShapeView.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/LineBackgroundShapeView.swift @@ -3,12 +3,12 @@ import SwiftUI struct LineBackgroundShapeView: View { var chartData: ChartData var geometry: GeometryProxy - var style: ChartStyle + var backgroundColor: ColorGradient var body: some View { LineBackgroundShape(data: chartData.normalisedData) - .fill(LinearGradient(gradient: Gradient(colors: [style.backgroundColor.startColor, - style.foregroundColor.first?.startColor ?? .white]), + .fill(LinearGradient(gradient: Gradient(colors: [backgroundColor.startColor, + backgroundColor.endColor]), startPoint: .bottom, endPoint: .top)) .rotationEffect(.degrees(180), anchor: .center) diff --git a/Sources/SwiftUICharts/Charts/LineChart/LineShapeView.swift b/Sources/SwiftUICharts/Charts/LineChart/LineShapeView.swift index b37fed7c..1a81087b 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/LineShapeView.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/LineShapeView.swift @@ -13,6 +13,16 @@ struct LineShapeView: View, Animatable { set { trimTo = Double(newValue) } } + var chartMarkColor: LinearGradient { + if let customColor = chartProperties.customChartMarksColors { + return customColor.linearGradient(from: .leading, to: .trailing) + } + + return LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient, + startPoint: .leading, + endPoint: .trailing) + } + var body: some View { ZStack { LineShape(data: chartData.normalisedData, lineStyle: chartProperties.lineStyle) @@ -28,9 +38,7 @@ struct LineShapeView: View, Animatable { MarkerShape(data: chartData.normalisedData) .trim(from: 0, to: CGFloat(trimTo)) .fill(.white, - strokeBorder: LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient, - startPoint: .leading, - endPoint: .trailing), + strokeBorder: chartMarkColor, lineWidth: chartProperties.lineWidth) .rotationEffect(.degrees(180), anchor: .center) .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) diff --git a/Sources/SwiftUICharts/Charts/LineChart/Model/LineChartProperties.swift b/Sources/SwiftUICharts/Charts/LineChart/Model/LineChartProperties.swift index 82ee46ad..59cc9060 100644 --- a/Sources/SwiftUICharts/Charts/LineChart/Model/LineChartProperties.swift +++ b/Sources/SwiftUICharts/Charts/LineChart/Model/LineChartProperties.swift @@ -2,8 +2,9 @@ import SwiftUI public class LineChartProperties: ObservableObject { @Published var lineWidth: CGFloat = 2.0 - @Published var showBackground: Bool = false + @Published var backgroundGradient: ColorGradient? @Published var showChartMarks: Bool = true + @Published var customChartMarksColors: ColorGradient? @Published var lineStyle: LineStyle = .curved public init() {