// // MeasurementChartView.swift // USB Meter // // Created by Bogdan Timofte on 06/05/2020. // Copyright © 2020 Bogdan Timofte. All rights reserved. // import SwiftUI struct MeasurementChartView: View { @EnvironmentObject private var measurements: Measurements @State var displayVoltage: Bool = false @State var displayCurrent: Bool = false @State var displayPower: Bool = true let xLabels: Int = 4 let yLabels: Int = 4 var body: some View { Group { //if measurements.power.points.count > 0 { VStack { HStack { Button( action: { self.displayVoltage.toggle() if self.displayVoltage { self.displayPower = false } } ) { Text("Voltage") } .asEnableFeatureButton(state: displayVoltage) Button( action: { self.displayCurrent.toggle() if self.displayCurrent { self.displayPower = false } } ) { Text("Current") } .asEnableFeatureButton(state: displayCurrent) Button( action: { self.displayPower.toggle() if self.displayPower { self.displayCurrent = false self.displayVoltage = false } } ) { Text("Power") } .asEnableFeatureButton(state: displayPower) } .padding(.bottom, 5) if measurements.current.context.isValid { VStack { GeometryReader { geometry in HStack { Group { // MARK: Left Legend if self.displayPower { self.yAxisLabelsView(geometry: geometry, context: self.measurements.power.context, measurementUnit: "W") .withRoundedRectangleBackground(cornerRadius: 0, foregroundColor: .red, opacity: 0.5) } else if self.displayVoltage { self.yAxisLabelsView(geometry: geometry, context: self.measurements.voltage.context, measurementUnit: "V") .withRoundedRectangleBackground(cornerRadius: 0, foregroundColor: .green, opacity: 0.5) } else if self.displayCurrent { self.yAxisLabelsView(geometry: geometry, context: self.measurements.current.context, measurementUnit: "A") .withRoundedRectangleBackground(cornerRadius: 0, foregroundColor: .blue, opacity: 0.5) } } ZStack { // MARK: Graph if self.displayPower { Chart(strokeColor: .red) .environmentObject(self.measurements.power) .opacity(0.5) } else { if self.displayVoltage{ Chart(strokeColor: .green) .environmentObject(self.measurements.voltage) .opacity(0.5) } if self.displayCurrent{ Chart(strokeColor: .blue) .environmentObject(self.measurements.current) .opacity(0.5) } } // MARK: Grid self.horizontalGuides() self.verticalGuides() } .withRoundedRectangleBackground( cornerRadius: 0, foregroundColor: .primary, opacity: 0.06 ) Group { // MARK: Right Legend self.yAxisLabelsView(geometry: geometry, context: self.measurements.current.context, measurementUnit: "A") .foregroundColor(self.displayVoltage && self.displayCurrent ? .primary : .clear) .withRoundedRectangleBackground(cornerRadius: 0, foregroundColor: .blue, opacity: 0.5) } } } xAxisLabelsView(context: self.measurements.current.context) .padding(.horizontal, 10) } } else { Text("Nothing to show!") } } .padding(10) .font(.footnote) .frame(maxWidth: .greatestFiniteMagnitude) .withRoundedRectangleBackground( cornerRadius: 15, foregroundColor: .primary, opacity: 0.03 ) .padding() } } // MARK: Cu iteratie nu functioneaza deoarece view-ul din bucla nu este reimprospatat la modificare obiectului observat fileprivate func xAxisLabelsView(context: ChartContext) -> some View { var timeFormat: String? switch context.size.width { case 0..<3600: timeFormat = "HH:mm:ss" case 3600...86400: timeFormat = "HH:mm" default: timeFormat = "E:HH:MM" } return HStack { ForEach (1...xLabels, id: \.self) { i in Group { Text( "\(Date(timeIntervalSince1970: context.xAxisLabel(for: i, of: self.yLabels)).format(as: timeFormat!))" ) .fontWeight(.semibold) if i < self.xLabels { Spacer() } } } } } fileprivate func yAxisLabelsView(geometry: GeometryProxy, context: ChartContext, measurementUnit: String) -> some View { return ZStack { VStack { Text("\(context.yAxisLabel(for: 4, of: 4).format(fractionDigits: 2))") .fontWeight(.semibold) .padding(.top, geometry.size.height*Constants.chartUnderscan/2 ) Spacer() ForEach (1.. some View { GeometryReader { geometry in Path { path in let pading = geometry.size.height*Constants.chartUnderscan let height = geometry.size.height - pading let border = pading/2 for i: CGFloat in stride(from: 0, through: CGFloat(self.yLabels-1), by: 1) { path.addLine(from: CGPoint(x: 0, y: border + height*i/CGFloat(self.yLabels-1 )), to: CGPoint(x: geometry.size.width, y: border + height*i/CGFloat(self.yLabels-1))) } }.stroke(lineWidth: 0.25) } } fileprivate func verticalGuides() -> some View { GeometryReader { geometry in Path { path in for i: CGFloat in stride(from: 1, through: CGFloat(self.xLabels-1), by: 1) { path.move(to: CGPoint(x: geometry.size.width*i/CGFloat(self.xLabels-1), y: 0) ) path.addLine(to: CGPoint(x: path.currentPoint!.x, y: geometry.size.height) ) } }.stroke(lineWidth: 0.25) } } } struct Chart : View { @EnvironmentObject private var measurement: Measurements.Measurement var areaChart: Bool = false var strokeColor: Color = .black var body : some View { GeometryReader { geometry in if self.areaChart { self.path( geometry: geometry ) .fill(LinearGradient( gradient: .init(colors: [Color.red, Color.green]), startPoint: .init(x: 0.5, y: 0.1), endPoint: .init(x: 0.5, y: 0.9))) } else { self.path( geometry: geometry ) .stroke(self.strokeColor, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round)) } } } fileprivate func path(geometry: GeometryProxy) -> Path { return Path { path in let firstPoint = measurement.context.placeInRect(point: measurement.points.first!.point()) path.move(to: CGPoint(x: firstPoint.x * geometry.size.width, y: firstPoint.y * geometry.size.height ) ) for item in measurement.points.map({ measurement.context.placeInRect(point: $0.point()) }) { path.addLine(to: CGPoint(x: item.x * geometry.size.width, y: item.y * geometry.size.height ) ) } if self.areaChart { let lastPointX = measurement.context.placeInRect(point: CGPoint(x: measurement.points.last!.point().x, y: measurement.context.origin.y )) let firstPointX = measurement.context.placeInRect(point: CGPoint(x: measurement.points.first!.point().x, y: measurement.context.origin.y )) path.addLine(to: CGPoint(x: lastPointX.x * geometry.size.width, y: lastPointX.y * geometry.size.height ) ) path.addLine(to: CGPoint(x: firstPointX.x * geometry.size.width, y: firstPointX.y * geometry.size.height ) ) // MARK: Nu e nevoie. Fill inchide automat calea // path.closeSubpath() } } } }