| 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 |
} |