diff --git a/Sources/SwiftUICharts/LineChart/Legend.swift b/Sources/SwiftUICharts/LineChart/Legend.swift index b613cb06..bce55800 100644 --- a/Sources/SwiftUICharts/LineChart/Legend.swift +++ b/Sources/SwiftUICharts/LineChart/Legend.swift @@ -13,87 +13,90 @@ struct Legend: View { @Binding var frame: CGRect @Binding var hideHorizontalLines: Bool @Environment(\.colorScheme) var colorScheme: ColorScheme - let padding:CGFloat = 3 - + var showOrigin: Bool? + let padding: CGFloat = 3 + var stepWidth: CGFloat { if data.points.count < 2 { return 0 } - return frame.size.width / CGFloat(data.points.count-1) + return frame.size.width / CGFloat(data.points.count - 1) } + var stepHeight: CGFloat { - let points = self.data.onlyPoints() + let points = data.onlyPoints() if let min = points.min(), let max = points.max(), min != max { - if (min < 0){ - return (frame.size.height-padding) / CGFloat(max - min) - }else{ - return (frame.size.height-padding) / CGFloat(max + min) + if min < 0 { + return (frame.size.height - padding) / CGFloat(max - min) + } else { + return (frame.size.height - padding) / CGFloat(max + min) } } return 0 } var min: CGFloat { - let points = self.data.onlyPoints() + let points = data.onlyPoints() return CGFloat(points.min() ?? 0) } var body: some View { - ZStack(alignment: .topLeading){ - ForEach((0...4), id: \.self) { height in - HStack(alignment: .center){ - Text("\(self.getYLegendSafe(height: height), specifier: "%.2f")").offset(x: 0, y: self.getYposition(height: height) ) + ZStack(alignment: .bottomLeading) { + ForEach(showOrigin == true ? 0...5 : 0...4, id: \.self) { height in + HStack(alignment: .center) { + Text("\(self.getYLegendSafe(height: height), specifier: "%.2f")") + .offset(x: 0, y: self.getYposition(height: height)) .foregroundColor(Colors.LegendText) .font(.caption) - self.line(atHeight: self.getYLegendSafe(height: height), width: self.frame.width) - .stroke(self.colorScheme == .dark ? Colors.LegendDarkColor : Colors.LegendColor, style: StrokeStyle(lineWidth: 1.5, lineCap: .round, dash: [5,height == 0 ? 0 : 10])) + self.line(atHeight: self.getYLegendSafe(height: height), width: self.frame.width - 32) + .stroke(self.colorScheme == .dark ? Colors.LegendDarkColor : Colors.LegendColor, style: StrokeStyle(lineWidth: 1.5, lineCap: .round, dash:[5, height == 0 ? 0 : 10])) .opacity((self.hideHorizontalLines && height != 0) ? 0 : 1) .rotationEffect(.degrees(180), anchor: .center) .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) .animation(.easeOut(duration: 0.2)) - .clipped() } - } - } } - func getYLegendSafe(height:Int)->CGFloat{ + + func getYLegendSafe(height: Int) -> CGFloat { if let legend = getYLegend() { return CGFloat(legend[height]) } return 0 } - func getYposition(height: Int)-> CGFloat { + func getYposition(height: Int) -> CGFloat { if let legend = getYLegend() { - return (self.frame.height-((CGFloat(legend[height]) - min)*self.stepHeight))-(self.frame.height/2) + return (frame.height - ((CGFloat(legend[height]) - min) * stepHeight)) - (frame.height / 2) } return 0 - } func line(atHeight: CGFloat, width: CGFloat) -> Path { var hLine = Path() - hLine.move(to: CGPoint(x:5, y: (atHeight-min)*stepHeight)) - hLine.addLine(to: CGPoint(x: width, y: (atHeight-min)*stepHeight)) + hLine.move(to: CGPoint(x: 5, y: (atHeight - min) * stepHeight)) + hLine.addLine(to: CGPoint(x: width, y: (atHeight - min) * stepHeight)) return hLine } func getYLegend() -> [Double]? { - let points = self.data.onlyPoints() + let points = data.onlyPoints() guard let max = points.max() else { return nil } guard let min = points.min() else { return nil } - let step = Double(max - min)/4 - return [min+step * 0, min+step * 1, min+step * 2, min+step * 3, min+step * 4] + let step = Double(max - min) / 4 + guard showOrigin == true else { + return [min + step * 0, min + step * 1, min + step * 2, min + step * 3, min + step * 4] + } + return [0.00, min + step * 0, min + step * 1, min + step * 2, min + step * 3, min + step * 4] } } struct Legend_Previews: PreviewProvider { static var previews: some View { - GeometryReader{ geometry in - Legend(data: ChartData(points: [0.2,0.4,1.4,4.5]), frame: .constant(geometry.frame(in: .local)), hideHorizontalLines: .constant(false)) + GeometryReader { geometry in + Legend(data: ChartData(points: [0.2, 0.4, 1.4, 4.5]), frame: .constant(geometry.frame(in: .local)), hideHorizontalLines: .constant(false), showOrigin: true) }.frame(width: 320, height: 200) } } diff --git a/Sources/SwiftUICharts/LineChart/LineChartView.swift b/Sources/SwiftUICharts/LineChart/LineChartView.swift index 2726f083..fb8195b3 100644 --- a/Sources/SwiftUICharts/LineChart/LineChartView.swift +++ b/Sources/SwiftUICharts/LineChart/LineChartView.swift @@ -92,7 +92,6 @@ public struct LineChartView: View { Spacer() Text("\(self.currentValue, specifier: self.valueSpecifier)") .font(.system(size: 41, weight: .bold, design: .default)) - .offset(x: 0, y: 30) Spacer() } .transition(.scale) @@ -109,7 +108,7 @@ public struct LineChartView: View { } .frame(width: frame.width, height: frame.height + 30) .clipShape(RoundedRectangle(cornerRadius: 20)) - .offset(x: 0, y: 0) + .offset(x: 0, y: -30) }.frame(width: self.formSize.width, height: self.formSize.height) } .gesture(DragGesture() diff --git a/Sources/SwiftUICharts/LineChart/LineView.swift b/Sources/SwiftUICharts/LineChart/LineView.swift index 0ca0a527..3679dae0 100644 --- a/Sources/SwiftUICharts/LineChart/LineView.swift +++ b/Sources/SwiftUICharts/LineChart/LineView.swift @@ -10,109 +10,127 @@ import SwiftUI public struct LineView: View { @ObservedObject var data: ChartData + public var xAxisLables: [String]? public var title: String? public var legend: String? + public var showOrigin: Bool? public var style: ChartStyle public var darkModeStyle: ChartStyle - public var valueSpecifier:String + public var valueSpecifier: String @Environment(\.colorScheme) var colorScheme: ColorScheme @State private var showLegend = false - @State private var dragLocation:CGPoint = .zero - @State private var indicatorLocation:CGPoint = .zero + @State private var dragLocation: CGPoint = .zero + @State private var indicatorLocation: CGPoint = .zero @State private var closestPoint: CGPoint = .zero - @State private var opacity:Double = 0 + @State private var opacity: Double = 0 @State private var currentDataNumber: Double = 0 + @State private var currentDataLabel: String = "" @State private var hideHorizontalLines: Bool = false public init(data: [Double], + xAxisLables: [String]? = [], title: String? = nil, legend: String? = nil, + showOrigin: Bool? = true, style: ChartStyle = Styles.lineChartStyleOne, valueSpecifier: String? = "%.1f") { - self.data = ChartData(points: data) + self.xAxisLables = xAxisLables self.title = title self.legend = legend + self.showOrigin = showOrigin self.style = style self.valueSpecifier = valueSpecifier! self.darkModeStyle = style.darkModeStyle != nil ? style.darkModeStyle! : Styles.lineViewDarkMode } public var body: some View { - GeometryReader{ geometry in + GeometryReader { geometry in VStack(alignment: .leading, spacing: 8) { - Group{ - if (self.title != nil){ + Group { + if self.title != nil { Text(self.title!) .font(.title) .bold().foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor) } - if (self.legend != nil){ + if self.legend != nil { Text(self.legend!) .font(.callout) .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor) } - }.offset(x: 0, y: 20) - ZStack{ - GeometryReader{ reader in + } + ZStack { + GeometryReader { reader in Rectangle() .foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.backgroundColor : self.style.backgroundColor) - if(self.showLegend){ + if self.showLegend { Legend(data: self.data, - frame: .constant(reader.frame(in: .local)), hideHorizontalLines: self.$hideHorizontalLines) + frame: .constant(reader.frame(in: .local)), hideHorizontalLines: self.$hideHorizontalLines, showOrigin: self.showOrigin) .transition(.opacity) .animation(Animation.easeOut(duration: 1).delay(1)) } Line(data: self.data, - frame: .constant(CGRect(x: 0, y: 0, width: reader.frame(in: .local).width - 30, height: reader.frame(in: .local).height)), + frame: .constant(CGRect(x: 0, y: 0, width: reader.frame(in: .local).width - 32, height: reader.frame(in: .local).height)), touchLocation: self.$indicatorLocation, showIndicator: self.$hideHorizontalLines, minDataValue: .constant(nil), maxDataValue: .constant(nil), - showBackground: false - ) - .offset(x: 30, y: 0) - .onAppear(){ - self.showLegend = true + showBackground: false) + .offset(x: 32, y: 0) + .onAppear { + self.showLegend = true } - .onDisappear(){ + .onDisappear { self.showLegend = false } } .frame(width: geometry.frame(in: .local).size.width, height: 240) - .offset(x: 0, y: 40 ) - MagnifierRect(currentNumber: self.$currentDataNumber, valueSpecifier: self.valueSpecifier) + MagnifierRect(currentNumber: self.$currentDataNumber, currentDataLabel: self.$currentDataLabel, valueSpecifier: self.valueSpecifier) .opacity(self.opacity) - .offset(x: self.dragLocation.x - geometry.frame(in: .local).size.width/2, y: 36) + .offset(x: self.dragLocation.x - geometry.frame(in: .local).size.width / 2, y: 32) } .frame(width: geometry.frame(in: .local).size.width, height: 240) .gesture(DragGesture() - .onChanged({ value in + .onChanged { value in self.dragLocation = value.location - self.indicatorLocation = CGPoint(x: max(value.location.x-30,0), y: 32) + self.indicatorLocation = CGPoint(x: max(value.location.x - 30, 0), y: 32) self.opacity = 1 - self.closestPoint = self.getClosestDataPoint(toPoint: value.location, width: geometry.frame(in: .local).size.width-30, height: 240) + self.closestPoint = self.getClosestDataPoint(toPoint: value.location, width: geometry.frame(in: .local).size.width - 30, height: 240) self.hideHorizontalLines = true - }) - .onEnded({ value in - self.opacity = 0 - self.hideHorizontalLines = false - }) + } + .onEnded { _ in + self.opacity = 0 + self.hideHorizontalLines = false + } ) + if self.xAxisLables != nil { + HStack{ + ForEach(self.xAxisLables!, id: \.self) { xAxisLable in + Text(xAxisLable) + .foregroundColor(Colors.LegendText) + .font(.caption) + } + } + .padding(.trailing, 32) + .offset(x: 32, y: self.showOrigin == true ? 26 : 0) + } } } } - func getClosestDataPoint(toPoint: CGPoint, width:CGFloat, height: CGFloat) -> CGPoint { + func getClosestDataPoint(toPoint: CGPoint, width: CGFloat, height: CGFloat) -> CGPoint { let points = self.data.onlyPoints() - let stepWidth: CGFloat = width / CGFloat(points.count-1) + let stepWidth: CGFloat = width / CGFloat(points.count - 1) let stepHeight: CGFloat = height / CGFloat(points.max()! + points.min()!) - let index:Int = Int(floor((toPoint.x-15)/stepWidth)) - if (index >= 0 && index < points.count){ + let index: Int = Int(floor((toPoint.x - 15) / stepWidth)) + if let xAxisLables = xAxisLables, index >= 0 && index < xAxisLables.count { + self.currentDataLabel = xAxisLables[index] + } + if index >= 0 && index < points.count { self.currentDataNumber = points[index] - return CGPoint(x: CGFloat(index)*stepWidth, y: CGFloat(points[index])*stepHeight) + return CGPoint(x: CGFloat(index) * stepWidth, y: CGFloat(points[index]) * stepHeight) } return .zero } @@ -120,7 +138,6 @@ public struct LineView: View { struct LineView_Previews: PreviewProvider { static var previews: some View { - LineView(data: [8,23,54,32,12,37,7,23,43], title: "Full chart", style: Styles.lineChartStyleOne) + LineView(data: [8, 23, 54, 32, 12, 37, 7, 23, 43], xAxisLables: [], title: "Full chart", legend: "Full chart", showOrigin: true, style: Styles.lineChartStyleOne) } } - diff --git a/Sources/SwiftUICharts/LineChart/MagnifierRect.swift b/Sources/SwiftUICharts/LineChart/MagnifierRect.swift index 4d3fd869..714903b9 100644 --- a/Sources/SwiftUICharts/LineChart/MagnifierRect.swift +++ b/Sources/SwiftUICharts/LineChart/MagnifierRect.swift @@ -9,14 +9,21 @@ import SwiftUI public struct MagnifierRect: View { @Binding var currentNumber: Double + @Binding var currentDataLabel: String var valueSpecifier:String @Environment(\.colorScheme) var colorScheme: ColorScheme public var body: some View { ZStack{ - Text("\(self.currentNumber, specifier: valueSpecifier)") + VStack { + Text("\(self.currentNumber, specifier: valueSpecifier)") + .font(.system(size: 18, weight: .bold)) + .offset(x: 0, y:-110) + .foregroundColor(self.colorScheme == .dark ? Color.white : Color.black) + Text(currentDataLabel) .font(.system(size: 18, weight: .bold)) .offset(x: 0, y:-110) .foregroundColor(self.colorScheme == .dark ? Color.white : Color.black) + } if (self.colorScheme == .dark ){ RoundedRectangle(cornerRadius: 16) .stroke(Color.white, lineWidth: self.colorScheme == .dark ? 2 : 0)