Newer Older
226 lines | 10.779kb
Bogdan Timofte authored 2 weeks ago
1
//
2
//  MeasurementChartView.swift
3
//  USB Meter
4
//
5
//  Created by Bogdan Timofte on 06/05/2020.
6
//  Copyright © 2020 Bogdan Timofte. All rights reserved.
7
//
8

            
9
import SwiftUI
10

            
11
struct MeasurementChartView: View {
12

            
13
    @EnvironmentObject private var measurements: Measurements
14

            
15
    @State var displayVoltage: Bool = false
16
    @State var displayCurrent: Bool = false
17
    @State var displayPower: Bool = true
18
    let xLabels: Int = 4
19
    let yLabels: Int = 4
20

            
21
    var body: some View {
22
        Group {
23
            //if measurements.power.points.count > 0 {
24
            VStack {
25
                HStack {
26
                    Button( action: {
27
                        self.displayVoltage.toggle()
28
                        if self.displayVoltage {
29
                            self.displayPower = false
30
                        }
31
                    } ) { Text("Voltage") }
32
                        .asEnableFeatureButton(state: displayVoltage)
33
                    Button( action: {
34
                        self.displayCurrent.toggle()
35
                        if self.displayCurrent {
36
                            self.displayPower = false
37
                        }
38
                    } ) { Text("Current") }
39
                        .asEnableFeatureButton(state: displayCurrent)
40
                    Button( action: {
41
                        self.displayPower.toggle()
42
                        if self.displayPower {
43
                            self.displayCurrent = false
44
                            self.displayVoltage = false
45
                        }
46
                    } ) { Text("Power") }
47
                        .asEnableFeatureButton(state: displayPower)
48
                }
49
                .padding(.bottom, 5)
50
                if measurements.current.context.isValid {
51
                    VStack {
52
                        GeometryReader { geometry in
53
                            HStack {
54
                                Group { // MARK: Left Legend
55
                                    if self.displayPower {
56
                                        self.yAxisLabelsView(geometry: geometry, context: self.measurements.power.context, measurementUnit: "W")
57
                                            .withRoundedRectangleBackground(cornerRadius: 0, foregroundColor: .red, opacity: 0.5)
58
                                    } else if self.displayVoltage {
59
                                        self.yAxisLabelsView(geometry: geometry, context: self.measurements.voltage.context, measurementUnit: "V")
60
                                            .withRoundedRectangleBackground(cornerRadius: 0, foregroundColor: .green, opacity: 0.5)
61
                                    }
62
                                    else if self.displayCurrent {
63
                                        self.yAxisLabelsView(geometry: geometry, context: self.measurements.current.context, measurementUnit: "A")
64
                                            .withRoundedRectangleBackground(cornerRadius: 0, foregroundColor: .blue, opacity: 0.5)
65
                                    }
66
                                }
67
                                ZStack { // MARK: Graph
68
                                    if self.displayPower {
69
                                        Chart(strokeColor: .red)
70
                                            .environmentObject(self.measurements.power)
71
                                            .opacity(0.5)
72
                                    } else {
73
                                        if self.displayVoltage{
74
                                            Chart(strokeColor: .green)
75
                                                .environmentObject(self.measurements.voltage)
76
                                                .opacity(0.5)
77
                                        }
78
                                        if self.displayCurrent{
79
                                            Chart(strokeColor: .blue)
80
                                                .environmentObject(self.measurements.current)
81
                                                .opacity(0.5)
82
                                        }
83
                                    }
84

            
85
                                    // MARK: Grid
86
                                    self.horizontalGuides()
87
                                    self.verticalGuides()
88
                                }
89
                                .withRoundedRectangleBackground( cornerRadius: 0, foregroundColor: .primary, opacity: 0.06 )
90
                                Group { // MARK: Right Legend
91
                                    self.yAxisLabelsView(geometry: geometry, context: self.measurements.current.context, measurementUnit: "A")
92
                                        .foregroundColor(self.displayVoltage && self.displayCurrent ? .primary : .clear)
93
                                        .withRoundedRectangleBackground(cornerRadius: 0, foregroundColor: .blue, opacity: 0.5)
94
                                }
95
                            }
96
                        }
97
                        xAxisLabelsView(context: self.measurements.current.context)
98
                            .padding(.horizontal, 10)
99

            
100
                    }
101
                }
102
                else {
103
                    Text("Nothing to show!")
104
                }
105

            
106
            }
107
            .padding(10)
108
            .font(.footnote)
109
            .frame(maxWidth: .greatestFiniteMagnitude)
110
            .withRoundedRectangleBackground( cornerRadius: 15, foregroundColor: .primary, opacity: 0.03 )
111
            .padding()
112
        }
113
    }
114

            
115
    // MARK: Cu iteratie nu functioneaza deoarece view-ul din bucla nu este reimprospatat la modificare obiectului observat
116
    fileprivate func xAxisLabelsView(context: ChartContext) -> some View {
117
        var timeFormat: String?
118
        switch context.size.width {
119
        case 0..<3600: timeFormat = "HH:mm:ss"
120
        case 3600...86400: timeFormat = "HH:mm"
121
        default: timeFormat = "E:HH:MM"
122
        }
123
        return HStack {
124
            ForEach (1...xLabels, id: \.self) { i in
125
                Group {
126
                    Text( "\(Date(timeIntervalSince1970: context.xAxisLabel(for: i, of: self.yLabels)).format(as: timeFormat!))" )
127
                        .fontWeight(.semibold)
128
                    if i < self.xLabels {
129
                        Spacer()
130
                    }
131
                }
132
            }
133
        }
134
    }
135

            
136
    fileprivate func yAxisLabelsView(geometry: GeometryProxy, context: ChartContext, measurementUnit: String) -> some View {
137
        return ZStack {
138
            VStack {
139
                Text("\(context.yAxisLabel(for: 4, of: 4).format(fractionDigits: 2))")
140
                    .fontWeight(.semibold)
141
                    .padding(.top, geometry.size.height*Constants.chartUnderscan/2 )
142
                Spacer()
143
                ForEach (1..<yLabels-1, id: \.self) { i in
144
                    Group {
145
                        Text("\(context.yAxisLabel(for: self.yLabels-i, of: self.yLabels).format(fractionDigits: 2))")
146
                            .fontWeight(.semibold)
147
                        Spacer()
148
                    }
149
                }
150
                Text("\(context.yAxisLabel(for: 1, of: yLabels).format(fractionDigits: 2))")
151
                    .fontWeight(.semibold)
152
                    .padding(.bottom, geometry.size.height*Constants.chartUnderscan/2 )
153
            }
154
            VStack {
155
                Text(measurementUnit)
156
                    .fontWeight(.bold)
157
                    .padding(.top, 5)
158
                Spacer()
159
            }
160
        }
161
    }
162

            
163
    fileprivate func horizontalGuides() -> some View {
164
        GeometryReader { geometry in
165
            Path { path in
166
                let pading = geometry.size.height*Constants.chartUnderscan
167
                let height = geometry.size.height - pading
168
                let border = pading/2
169
                for i: CGFloat in stride(from: 0, through: CGFloat(self.yLabels-1), by: 1) {
170
                    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)))
171
                }
172
            }.stroke(lineWidth: 0.25)
173
        }
174
    }
175

            
176
    fileprivate func verticalGuides() -> some View {
177
        GeometryReader { geometry in
178
            Path { path in
179

            
180
                for i: CGFloat in stride(from: 1, through: CGFloat(self.xLabels-1), by: 1) {
181
                    path.move(to: CGPoint(x: geometry.size.width*i/CGFloat(self.xLabels-1), y: 0) )
182
                    path.addLine(to: CGPoint(x: path.currentPoint!.x, y: geometry.size.height) )
183
                }
184
            }.stroke(lineWidth: 0.25)
185
        }
186
    }
187

            
188
}
189

            
190
struct Chart : View {
191

            
192
    @EnvironmentObject private var measurement: Measurements.Measurement
193
    var areaChart: Bool = false
194
    var strokeColor: Color = .black
195

            
196
    var body : some View {
197
        GeometryReader { geometry in
198
            if self.areaChart {
199
                self.path( geometry: geometry )
200
                    .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)))
201
            } else {
202
                self.path( geometry: geometry )
203
                    .stroke(self.strokeColor, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
204
            }
205
        }
206
    }
207

            
208
    fileprivate func path(geometry: GeometryProxy) -> Path {
209
        return Path { path in
210
            let firstPoint = measurement.context.placeInRect(point: measurement.points.first!.point())
211
            path.move(to: CGPoint(x: firstPoint.x * geometry.size.width, y: firstPoint.y * geometry.size.height ) )
212
            for item in measurement.points.map({ measurement.context.placeInRect(point: $0.point()) }) {
213
                path.addLine(to: CGPoint(x: item.x * geometry.size.width, y: item.y * geometry.size.height ) )
214
            }
215
            if self.areaChart {
216
                let lastPointX = measurement.context.placeInRect(point: CGPoint(x: measurement.points.last!.point().x, y: measurement.context.origin.y ))
217
                let firstPointX = measurement.context.placeInRect(point: CGPoint(x: measurement.points.first!.point().x, y: measurement.context.origin.y ))
218
                path.addLine(to: CGPoint(x: lastPointX.x * geometry.size.width, y: lastPointX.y * geometry.size.height ) )
219
                path.addLine(to: CGPoint(x: firstPointX.x * geometry.size.width, y: firstPointX.y * geometry.size.height ) )
220
                // MARK: Nu e nevoie. Fill inchide automat calea
221
                // path.closeSubpath()
222
            }
223
        }
224
    }
225

            
226
}