|
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 {
|
|
Bogdan Timofte
authored
2 weeks ago
|
12
|
private let minimumTimeSpan: TimeInterval = 1
|
|
Bogdan Timofte
authored
2 weeks ago
|
13
|
private let minimumVoltageSpan = 0.5
|
|
|
14
|
private let minimumCurrentSpan = 0.5
|
|
|
15
|
private let minimumPowerSpan = 0.5
|
|
|
16
|
private let axisColumnWidth: CGFloat = 46
|
|
|
17
|
private let chartSectionSpacing: CGFloat = 8
|
|
|
18
|
private let xAxisHeight: CGFloat = 28
|
|
Bogdan Timofte
authored
2 weeks ago
|
19
|
|
|
|
20
|
@EnvironmentObject private var measurements: Measurements
|
|
Bogdan Timofte
authored
2 weeks ago
|
21
|
var timeRange: ClosedRange<Date>? = nil
|
|
Bogdan Timofte
authored
2 weeks ago
|
22
|
|
|
|
23
|
@State var displayVoltage: Bool = false
|
|
|
24
|
@State var displayCurrent: Bool = false
|
|
|
25
|
@State var displayPower: Bool = true
|
|
|
26
|
let xLabels: Int = 4
|
|
|
27
|
let yLabels: Int = 4
|
|
|
28
|
|
|
|
29
|
var body: some View {
|
|
Bogdan Timofte
authored
2 weeks ago
|
30
|
let powerSeries = series(for: measurements.power, minimumYSpan: minimumPowerSpan)
|
|
|
31
|
let voltageSeries = series(for: measurements.voltage, minimumYSpan: minimumVoltageSpan)
|
|
|
32
|
let currentSeries = series(for: measurements.current, minimumYSpan: minimumCurrentSpan)
|
|
|
33
|
let primarySeries = displayedPrimarySeries(
|
|
|
34
|
powerSeries: powerSeries,
|
|
|
35
|
voltageSeries: voltageSeries,
|
|
|
36
|
currentSeries: currentSeries
|
|
|
37
|
)
|
|
|
38
|
|
|
Bogdan Timofte
authored
2 weeks ago
|
39
|
Group {
|
|
Bogdan Timofte
authored
2 weeks ago
|
40
|
if let primarySeries {
|
|
|
41
|
VStack(alignment: .leading, spacing: 12) {
|
|
|
42
|
chartToggleBar
|
|
|
43
|
|
|
|
44
|
GeometryReader { geometry in
|
|
|
45
|
let plotHeight = max(geometry.size.height - xAxisHeight, 140)
|
|
|
46
|
|
|
|
47
|
VStack(spacing: 6) {
|
|
|
48
|
HStack(spacing: chartSectionSpacing) {
|
|
|
49
|
primaryAxisView(
|
|
|
50
|
height: plotHeight,
|
|
|
51
|
powerSeries: powerSeries,
|
|
|
52
|
voltageSeries: voltageSeries,
|
|
|
53
|
currentSeries: currentSeries
|
|
|
54
|
)
|
|
|
55
|
.frame(width: axisColumnWidth, height: plotHeight)
|
|
|
56
|
|
|
|
57
|
ZStack {
|
|
|
58
|
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
|
|
59
|
.fill(Color.primary.opacity(0.05))
|
|
|
60
|
|
|
|
61
|
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
|
|
62
|
.stroke(Color.secondary.opacity(0.16), lineWidth: 1)
|
|
|
63
|
|
|
|
64
|
horizontalGuides(context: primarySeries.context)
|
|
|
65
|
verticalGuides(context: primarySeries.context)
|
|
|
66
|
renderedChart(
|
|
|
67
|
powerSeries: powerSeries,
|
|
|
68
|
voltageSeries: voltageSeries,
|
|
|
69
|
currentSeries: currentSeries
|
|
|
70
|
)
|
|
Bogdan Timofte
authored
2 weeks ago
|
71
|
}
|
|
Bogdan Timofte
authored
2 weeks ago
|
72
|
.clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
|
|
|
73
|
.frame(maxWidth: .infinity)
|
|
|
74
|
.frame(height: plotHeight)
|
|
|
75
|
|
|
|
76
|
secondaryAxisView(
|
|
|
77
|
height: plotHeight,
|
|
|
78
|
powerSeries: powerSeries,
|
|
|
79
|
voltageSeries: voltageSeries,
|
|
|
80
|
currentSeries: currentSeries
|
|
|
81
|
)
|
|
|
82
|
.frame(width: axisColumnWidth, height: plotHeight)
|
|
Bogdan Timofte
authored
2 weeks ago
|
83
|
}
|
|
Bogdan Timofte
authored
2 weeks ago
|
84
|
|
|
|
85
|
xAxisLabelsView(context: primarySeries.context)
|
|
|
86
|
.frame(height: xAxisHeight)
|
|
Bogdan Timofte
authored
2 weeks ago
|
87
|
}
|
|
Bogdan Timofte
authored
2 weeks ago
|
88
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
|
Bogdan Timofte
authored
2 weeks ago
|
89
|
}
|
|
|
90
|
}
|
|
Bogdan Timofte
authored
2 weeks ago
|
91
|
} else {
|
|
|
92
|
VStack(alignment: .leading, spacing: 12) {
|
|
|
93
|
chartToggleBar
|
|
Bogdan Timofte
authored
2 weeks ago
|
94
|
Text("Nothing to show!")
|
|
Bogdan Timofte
authored
2 weeks ago
|
95
|
.foregroundColor(.secondary)
|
|
Bogdan Timofte
authored
2 weeks ago
|
96
|
}
|
|
|
97
|
}
|
|
Bogdan Timofte
authored
2 weeks ago
|
98
|
}
|
|
|
99
|
.font(.footnote)
|
|
|
100
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
|
101
|
}
|
|
|
102
|
|
|
|
103
|
private var chartToggleBar: some View {
|
|
|
104
|
HStack(spacing: 8) {
|
|
|
105
|
Button(action: {
|
|
|
106
|
self.displayVoltage.toggle()
|
|
|
107
|
if self.displayVoltage {
|
|
|
108
|
self.displayPower = false
|
|
|
109
|
}
|
|
|
110
|
}) { Text("Voltage") }
|
|
|
111
|
.asEnableFeatureButton(state: displayVoltage)
|
|
|
112
|
|
|
|
113
|
Button(action: {
|
|
|
114
|
self.displayCurrent.toggle()
|
|
|
115
|
if self.displayCurrent {
|
|
|
116
|
self.displayPower = false
|
|
|
117
|
}
|
|
|
118
|
}) { Text("Current") }
|
|
|
119
|
.asEnableFeatureButton(state: displayCurrent)
|
|
|
120
|
|
|
|
121
|
Button(action: {
|
|
|
122
|
self.displayPower.toggle()
|
|
|
123
|
if self.displayPower {
|
|
|
124
|
self.displayCurrent = false
|
|
|
125
|
self.displayVoltage = false
|
|
|
126
|
}
|
|
|
127
|
}) { Text("Power") }
|
|
|
128
|
.asEnableFeatureButton(state: displayPower)
|
|
|
129
|
}
|
|
|
130
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
|
131
|
}
|
|
|
132
|
|
|
|
133
|
@ViewBuilder
|
|
|
134
|
private func primaryAxisView(
|
|
|
135
|
height: CGFloat,
|
|
|
136
|
powerSeries: (points: [Measurements.Measurement.Point], context: ChartContext),
|
|
|
137
|
voltageSeries: (points: [Measurements.Measurement.Point], context: ChartContext),
|
|
|
138
|
currentSeries: (points: [Measurements.Measurement.Point], context: ChartContext)
|
|
|
139
|
) -> some View {
|
|
|
140
|
if displayPower {
|
|
|
141
|
yAxisLabelsView(
|
|
|
142
|
height: height,
|
|
|
143
|
context: powerSeries.context,
|
|
|
144
|
measurementUnit: "W",
|
|
|
145
|
tint: .red
|
|
|
146
|
)
|
|
|
147
|
} else if displayVoltage {
|
|
|
148
|
yAxisLabelsView(
|
|
|
149
|
height: height,
|
|
|
150
|
context: voltageSeries.context,
|
|
|
151
|
measurementUnit: "V",
|
|
|
152
|
tint: .green
|
|
|
153
|
)
|
|
|
154
|
} else if displayCurrent {
|
|
|
155
|
yAxisLabelsView(
|
|
|
156
|
height: height,
|
|
|
157
|
context: currentSeries.context,
|
|
|
158
|
measurementUnit: "A",
|
|
|
159
|
tint: .blue
|
|
|
160
|
)
|
|
|
161
|
}
|
|
|
162
|
}
|
|
|
163
|
|
|
|
164
|
@ViewBuilder
|
|
|
165
|
private func renderedChart(
|
|
|
166
|
powerSeries: (points: [Measurements.Measurement.Point], context: ChartContext),
|
|
|
167
|
voltageSeries: (points: [Measurements.Measurement.Point], context: ChartContext),
|
|
|
168
|
currentSeries: (points: [Measurements.Measurement.Point], context: ChartContext)
|
|
|
169
|
) -> some View {
|
|
|
170
|
if self.displayPower {
|
|
|
171
|
Chart(points: powerSeries.points, context: powerSeries.context, strokeColor: .red)
|
|
|
172
|
.opacity(0.72)
|
|
|
173
|
} else {
|
|
|
174
|
if self.displayVoltage {
|
|
|
175
|
Chart(points: voltageSeries.points, context: voltageSeries.context, strokeColor: .green)
|
|
|
176
|
.opacity(0.78)
|
|
|
177
|
}
|
|
|
178
|
if self.displayCurrent {
|
|
|
179
|
Chart(points: currentSeries.points, context: currentSeries.context, strokeColor: .blue)
|
|
|
180
|
.opacity(0.78)
|
|
|
181
|
}
|
|
|
182
|
}
|
|
|
183
|
}
|
|
|
184
|
|
|
|
185
|
@ViewBuilder
|
|
|
186
|
private func secondaryAxisView(
|
|
|
187
|
height: CGFloat,
|
|
|
188
|
powerSeries: (points: [Measurements.Measurement.Point], context: ChartContext),
|
|
|
189
|
voltageSeries: (points: [Measurements.Measurement.Point], context: ChartContext),
|
|
|
190
|
currentSeries: (points: [Measurements.Measurement.Point], context: ChartContext)
|
|
|
191
|
) -> some View {
|
|
|
192
|
if displayVoltage && displayCurrent {
|
|
|
193
|
yAxisLabelsView(
|
|
|
194
|
height: height,
|
|
|
195
|
context: currentSeries.context,
|
|
|
196
|
measurementUnit: "A",
|
|
|
197
|
tint: .blue
|
|
|
198
|
)
|
|
|
199
|
} else {
|
|
|
200
|
primaryAxisView(
|
|
|
201
|
height: height,
|
|
|
202
|
powerSeries: powerSeries,
|
|
|
203
|
voltageSeries: voltageSeries,
|
|
|
204
|
currentSeries: currentSeries
|
|
|
205
|
)
|
|
Bogdan Timofte
authored
2 weeks ago
|
206
|
}
|
|
|
207
|
}
|
|
Bogdan Timofte
authored
2 weeks ago
|
208
|
|
|
|
209
|
private func displayedPrimarySeries(
|
|
|
210
|
powerSeries: (points: [Measurements.Measurement.Point], context: ChartContext),
|
|
|
211
|
voltageSeries: (points: [Measurements.Measurement.Point], context: ChartContext),
|
|
|
212
|
currentSeries: (points: [Measurements.Measurement.Point], context: ChartContext)
|
|
|
213
|
) -> (points: [Measurements.Measurement.Point], context: ChartContext)? {
|
|
|
214
|
if displayPower {
|
|
|
215
|
return powerSeries.points.isEmpty ? nil : powerSeries
|
|
|
216
|
}
|
|
|
217
|
if displayVoltage {
|
|
|
218
|
return voltageSeries.points.isEmpty ? nil : voltageSeries
|
|
|
219
|
}
|
|
|
220
|
if displayCurrent {
|
|
|
221
|
return currentSeries.points.isEmpty ? nil : currentSeries
|
|
|
222
|
}
|
|
|
223
|
return nil
|
|
|
224
|
}
|
|
|
225
|
|
|
|
226
|
private func series(
|
|
|
227
|
for measurement: Measurements.Measurement,
|
|
|
228
|
minimumYSpan: Double
|
|
|
229
|
) -> (points: [Measurements.Measurement.Point], context: ChartContext) {
|
|
|
230
|
let points = measurement.points.filter { point in
|
|
|
231
|
guard let timeRange else { return true }
|
|
|
232
|
return timeRange.contains(point.timestamp)
|
|
|
233
|
}
|
|
|
234
|
let context = ChartContext()
|
|
|
235
|
for point in points {
|
|
|
236
|
context.include(point: point.point())
|
|
|
237
|
}
|
|
|
238
|
if !points.isEmpty {
|
|
|
239
|
context.ensureMinimumSize(
|
|
|
240
|
width: CGFloat(minimumTimeSpan),
|
|
|
241
|
height: CGFloat(minimumYSpan)
|
|
|
242
|
)
|
|
|
243
|
}
|
|
|
244
|
return (points, context)
|
|
|
245
|
}
|
|
Bogdan Timofte
authored
2 weeks ago
|
246
|
|
|
|
247
|
private func yGuidePosition(
|
|
|
248
|
for labelIndex: Int,
|
|
|
249
|
context: ChartContext,
|
|
|
250
|
height: CGFloat
|
|
|
251
|
) -> CGFloat {
|
|
|
252
|
let value = context.yAxisLabel(for: labelIndex, of: yLabels)
|
|
|
253
|
let anchorPoint = CGPoint(x: context.origin.x, y: CGFloat(value))
|
|
|
254
|
return context.placeInRect(point: anchorPoint).y * height
|
|
|
255
|
}
|
|
|
256
|
|
|
|
257
|
private func xGuidePosition(
|
|
|
258
|
for labelIndex: Int,
|
|
|
259
|
context: ChartContext,
|
|
|
260
|
width: CGFloat
|
|
|
261
|
) -> CGFloat {
|
|
|
262
|
let value = context.xAxisLabel(for: labelIndex, of: xLabels)
|
|
|
263
|
let anchorPoint = CGPoint(x: CGFloat(value), y: context.origin.y)
|
|
|
264
|
return context.placeInRect(point: anchorPoint).x * width
|
|
|
265
|
}
|
|
Bogdan Timofte
authored
2 weeks ago
|
266
|
|
|
|
267
|
// MARK: Cu iteratie nu functioneaza deoarece view-ul din bucla nu este reimprospatat la modificare obiectului observat
|
|
Bogdan Timofte
authored
2 weeks ago
|
268
|
fileprivate func xAxisLabelsView(
|
|
|
269
|
context: ChartContext
|
|
|
270
|
) -> some View {
|
|
Bogdan Timofte
authored
2 weeks ago
|
271
|
var timeFormat: String?
|
|
|
272
|
switch context.size.width {
|
|
|
273
|
case 0..<3600: timeFormat = "HH:mm:ss"
|
|
|
274
|
case 3600...86400: timeFormat = "HH:mm"
|
|
Bogdan Timofte
authored
2 weeks ago
|
275
|
default: timeFormat = "E HH:mm"
|
|
|
276
|
}
|
|
|
277
|
let labels = (1...xLabels).map {
|
|
|
278
|
Date(timeIntervalSince1970: context.xAxisLabel(for: $0, of: self.xLabels)).format(as: timeFormat!)
|
|
Bogdan Timofte
authored
2 weeks ago
|
279
|
}
|
|
Bogdan Timofte
authored
2 weeks ago
|
280
|
|
|
|
281
|
return HStack(spacing: chartSectionSpacing) {
|
|
|
282
|
Color.clear
|
|
|
283
|
.frame(width: axisColumnWidth)
|
|
|
284
|
|
|
|
285
|
GeometryReader { geometry in
|
|
|
286
|
let labelWidth = max(
|
|
|
287
|
geometry.size.width / CGFloat(max(xLabels - 1, 1)),
|
|
|
288
|
1
|
|
|
289
|
)
|
|
|
290
|
|
|
|
291
|
ZStack(alignment: .topLeading) {
|
|
|
292
|
Path { path in
|
|
|
293
|
for labelIndex in 1...self.xLabels {
|
|
|
294
|
let x = xGuidePosition(
|
|
|
295
|
for: labelIndex,
|
|
|
296
|
context: context,
|
|
|
297
|
width: geometry.size.width
|
|
|
298
|
)
|
|
|
299
|
path.move(to: CGPoint(x: x, y: 0))
|
|
|
300
|
path.addLine(to: CGPoint(x: x, y: 6))
|
|
|
301
|
}
|
|
|
302
|
}
|
|
|
303
|
.stroke(Color.secondary.opacity(0.26), lineWidth: 0.75)
|
|
|
304
|
|
|
|
305
|
ForEach(Array(labels.enumerated()), id: \.offset) { item in
|
|
|
306
|
let labelIndex = item.offset + 1
|
|
|
307
|
let centerX = xGuidePosition(
|
|
|
308
|
for: labelIndex,
|
|
|
309
|
context: context,
|
|
|
310
|
width: geometry.size.width
|
|
|
311
|
)
|
|
|
312
|
|
|
|
313
|
Text(item.element)
|
|
|
314
|
.font(.caption.weight(.semibold))
|
|
|
315
|
.monospacedDigit()
|
|
|
316
|
.lineLimit(1)
|
|
|
317
|
.minimumScaleFactor(0.68)
|
|
|
318
|
.frame(width: labelWidth)
|
|
|
319
|
.position(
|
|
|
320
|
x: centerX,
|
|
|
321
|
y: geometry.size.height * 0.7
|
|
|
322
|
)
|
|
Bogdan Timofte
authored
2 weeks ago
|
323
|
}
|
|
|
324
|
}
|
|
Bogdan Timofte
authored
2 weeks ago
|
325
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
Bogdan Timofte
authored
2 weeks ago
|
326
|
}
|
|
Bogdan Timofte
authored
2 weeks ago
|
327
|
|
|
|
328
|
Color.clear
|
|
|
329
|
.frame(width: axisColumnWidth)
|
|
Bogdan Timofte
authored
2 weeks ago
|
330
|
}
|
|
|
331
|
}
|
|
|
332
|
|
|
Bogdan Timofte
authored
2 weeks ago
|
333
|
fileprivate func yAxisLabelsView(
|
|
|
334
|
height: CGFloat,
|
|
|
335
|
context: ChartContext,
|
|
|
336
|
measurementUnit: String,
|
|
|
337
|
tint: Color
|
|
|
338
|
) -> some View {
|
|
|
339
|
GeometryReader { geometry in
|
|
|
340
|
ZStack(alignment: .top) {
|
|
|
341
|
ForEach(0..<yLabels, id: \.self) { row in
|
|
|
342
|
let labelIndex = yLabels - row
|
|
|
343
|
|
|
|
344
|
Text("\(context.yAxisLabel(for: labelIndex, of: yLabels).format(fractionDigits: 1))")
|
|
|
345
|
.font(.caption2.weight(.semibold))
|
|
|
346
|
.monospacedDigit()
|
|
|
347
|
.lineLimit(1)
|
|
|
348
|
.minimumScaleFactor(0.72)
|
|
|
349
|
.frame(width: max(geometry.size.width - 6, 0))
|
|
|
350
|
.position(
|
|
|
351
|
x: geometry.size.width / 2,
|
|
|
352
|
y: yGuidePosition(
|
|
|
353
|
for: labelIndex,
|
|
|
354
|
context: context,
|
|
|
355
|
height: geometry.size.height
|
|
|
356
|
)
|
|
|
357
|
)
|
|
Bogdan Timofte
authored
2 weeks ago
|
358
|
}
|
|
Bogdan Timofte
authored
2 weeks ago
|
359
|
|
|
Bogdan Timofte
authored
2 weeks ago
|
360
|
Text(measurementUnit)
|
|
Bogdan Timofte
authored
2 weeks ago
|
361
|
.font(.caption2.weight(.bold))
|
|
|
362
|
.foregroundColor(tint)
|
|
|
363
|
.padding(.horizontal, 6)
|
|
|
364
|
.padding(.vertical, 4)
|
|
|
365
|
.background(
|
|
|
366
|
Capsule(style: .continuous)
|
|
|
367
|
.fill(tint.opacity(0.14))
|
|
|
368
|
)
|
|
|
369
|
.padding(.top, 6)
|
|
Bogdan Timofte
authored
2 weeks ago
|
370
|
}
|
|
|
371
|
}
|
|
Bogdan Timofte
authored
2 weeks ago
|
372
|
.frame(height: height)
|
|
|
373
|
.background(
|
|
|
374
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
|
375
|
.fill(tint.opacity(0.12))
|
|
|
376
|
)
|
|
|
377
|
.overlay(
|
|
|
378
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
|
379
|
.stroke(tint.opacity(0.20), lineWidth: 1)
|
|
|
380
|
)
|
|
Bogdan Timofte
authored
2 weeks ago
|
381
|
}
|
|
|
382
|
|
|
Bogdan Timofte
authored
2 weeks ago
|
383
|
fileprivate func horizontalGuides(context: ChartContext) -> some View {
|
|
Bogdan Timofte
authored
2 weeks ago
|
384
|
GeometryReader { geometry in
|
|
|
385
|
Path { path in
|
|
Bogdan Timofte
authored
2 weeks ago
|
386
|
for labelIndex in 1...self.yLabels {
|
|
|
387
|
let y = yGuidePosition(
|
|
|
388
|
for: labelIndex,
|
|
|
389
|
context: context,
|
|
|
390
|
height: geometry.size.height
|
|
|
391
|
)
|
|
|
392
|
path.addLine(from: CGPoint(x: 0, y: y), to: CGPoint(x: geometry.size.width, y: y))
|
|
Bogdan Timofte
authored
2 weeks ago
|
393
|
}
|
|
Bogdan Timofte
authored
2 weeks ago
|
394
|
}
|
|
|
395
|
.stroke(Color.secondary.opacity(0.38), lineWidth: 0.85)
|
|
Bogdan Timofte
authored
2 weeks ago
|
396
|
}
|
|
|
397
|
}
|
|
|
398
|
|
|
Bogdan Timofte
authored
2 weeks ago
|
399
|
fileprivate func verticalGuides(context: ChartContext) -> some View {
|
|
Bogdan Timofte
authored
2 weeks ago
|
400
|
GeometryReader { geometry in
|
|
|
401
|
Path { path in
|
|
|
402
|
|
|
Bogdan Timofte
authored
2 weeks ago
|
403
|
for labelIndex in 2..<self.xLabels {
|
|
|
404
|
let x = xGuidePosition(
|
|
|
405
|
for: labelIndex,
|
|
|
406
|
context: context,
|
|
|
407
|
width: geometry.size.width
|
|
|
408
|
)
|
|
|
409
|
path.move(to: CGPoint(x: x, y: 0) )
|
|
Bogdan Timofte
authored
2 weeks ago
|
410
|
path.addLine(to: CGPoint(x: path.currentPoint!.x, y: geometry.size.height) )
|
|
|
411
|
}
|
|
Bogdan Timofte
authored
2 weeks ago
|
412
|
}
|
|
|
413
|
.stroke(Color.secondary.opacity(0.34), style: StrokeStyle(lineWidth: 0.8, dash: [4, 4]))
|
|
Bogdan Timofte
authored
2 weeks ago
|
414
|
}
|
|
|
415
|
}
|
|
|
416
|
|
|
|
417
|
}
|
|
|
418
|
|
|
|
419
|
struct Chart : View {
|
|
|
420
|
|
|
Bogdan Timofte
authored
2 weeks ago
|
421
|
let points: [Measurements.Measurement.Point]
|
|
|
422
|
let context: ChartContext
|
|
Bogdan Timofte
authored
2 weeks ago
|
423
|
var areaChart: Bool = false
|
|
|
424
|
var strokeColor: Color = .black
|
|
|
425
|
|
|
|
426
|
var body : some View {
|
|
|
427
|
GeometryReader { geometry in
|
|
|
428
|
if self.areaChart {
|
|
|
429
|
self.path( geometry: geometry )
|
|
|
430
|
.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)))
|
|
|
431
|
} else {
|
|
|
432
|
self.path( geometry: geometry )
|
|
|
433
|
.stroke(self.strokeColor, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
|
|
|
434
|
}
|
|
|
435
|
}
|
|
|
436
|
}
|
|
|
437
|
|
|
|
438
|
fileprivate func path(geometry: GeometryProxy) -> Path {
|
|
|
439
|
return Path { path in
|
|
Bogdan Timofte
authored
2 weeks ago
|
440
|
guard let first = points.first else { return }
|
|
|
441
|
let firstPoint = context.placeInRect(point: first.point())
|
|
Bogdan Timofte
authored
2 weeks ago
|
442
|
path.move(to: CGPoint(x: firstPoint.x * geometry.size.width, y: firstPoint.y * geometry.size.height ) )
|
|
Bogdan Timofte
authored
2 weeks ago
|
443
|
for item in points.map({ context.placeInRect(point: $0.point()) }) {
|
|
Bogdan Timofte
authored
2 weeks ago
|
444
|
path.addLine(to: CGPoint(x: item.x * geometry.size.width, y: item.y * geometry.size.height ) )
|
|
|
445
|
}
|
|
|
446
|
if self.areaChart {
|
|
Bogdan Timofte
authored
2 weeks ago
|
447
|
let lastPointX = context.placeInRect(point: CGPoint(x: points.last!.point().x, y: context.origin.y ))
|
|
|
448
|
let firstPointX = context.placeInRect(point: CGPoint(x: points.first!.point().x, y: context.origin.y ))
|
|
Bogdan Timofte
authored
2 weeks ago
|
449
|
path.addLine(to: CGPoint(x: lastPointX.x * geometry.size.width, y: lastPointX.y * geometry.size.height ) )
|
|
|
450
|
path.addLine(to: CGPoint(x: firstPointX.x * geometry.size.width, y: firstPointX.y * geometry.size.height ) )
|
|
|
451
|
// MARK: Nu e nevoie. Fill inchide automat calea
|
|
|
452
|
// path.closeSubpath()
|
|
|
453
|
}
|
|
|
454
|
}
|
|
|
455
|
}
|
|
|
456
|
|
|
|
457
|
}
|