127 lines
4.1 KiB
Swift
127 lines
4.1 KiB
Swift
import SwiftUI
|
|
import Charts
|
|
|
|
struct OrpView: View {
|
|
@Bindable var state: AppState
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
controlsRow
|
|
Divider()
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
headerValues
|
|
chart
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
}
|
|
.padding()
|
|
}
|
|
}
|
|
|
|
// MARK: - Controls
|
|
|
|
private var controlsRow: some View {
|
|
HStack(spacing: 10) {
|
|
LabeledField("Stabilize s", text: $state.orpStabilize, width: 80)
|
|
|
|
Button("Measure ORP") { state.startOrp() }
|
|
.buttonStyle(ActionButtonStyle(color: .green))
|
|
|
|
Button("Clear") { state.clearOrpHistory() }
|
|
.buttonStyle(ActionButtonStyle(color: Color(red: 0.55, green: 0.3, blue: 0.3)))
|
|
|
|
Text("n=\(state.orpHistory.count)")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
|
|
Spacer()
|
|
}
|
|
.padding(.horizontal)
|
|
.padding(.vertical, 8)
|
|
}
|
|
|
|
// MARK: - Header
|
|
|
|
@ViewBuilder
|
|
private var headerValues: some View {
|
|
if let r = state.orpResult {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
Text(String(format: "ORP: %.0f mV", r.vOrpMv))
|
|
.font(.system(size: 40, weight: .bold, design: .rounded))
|
|
.foregroundStyle(.primary)
|
|
|
|
Text(String(format: "Temp: %.1f\u{00B0}C", r.tempC))
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
|
|
if let refR = state.orpRef {
|
|
let dV = r.vOrpMv - refR.vOrpMv
|
|
Text(String(format: "vs Ref: dORP=%+.1f mV (ref=%.0f mV)",
|
|
dV, refR.vOrpMv))
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
} else {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
Text("No measurement yet")
|
|
.font(.title3)
|
|
.foregroundStyle(.secondary)
|
|
Text("Open-circuit potential vs AgCl reference. Above ~650 mV indicates sanitary water.")
|
|
.font(.caption)
|
|
.foregroundStyle(Color(white: 0.4))
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Chart
|
|
|
|
@ViewBuilder
|
|
private var chart: some View {
|
|
if state.orpHistory.isEmpty {
|
|
RoundedRectangle(cornerRadius: 8)
|
|
.fill(Color.white.opacity(0.03))
|
|
.overlay(
|
|
Text("No samples yet")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
)
|
|
} else {
|
|
Chart(state.orpHistory) { sample in
|
|
LineMark(
|
|
x: .value("t", Double(sample.tS)),
|
|
y: .value("mV", Double(sample.vMv))
|
|
)
|
|
.foregroundStyle(Color.orange)
|
|
.lineStyle(StrokeStyle(lineWidth: 2))
|
|
|
|
PointMark(
|
|
x: .value("t", Double(sample.tS)),
|
|
y: .value("mV", Double(sample.vMv))
|
|
)
|
|
.foregroundStyle(Color.orange)
|
|
.symbolSize(30)
|
|
}
|
|
.chartXAxisLabel("t (s)")
|
|
.chartYAxisLabel("ORP (mV)")
|
|
.chartXAxis {
|
|
AxisMarks { _ in
|
|
AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5))
|
|
.foregroundStyle(Color.gray.opacity(0.3))
|
|
AxisValueLabel()
|
|
.font(.caption2)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.chartYAxis {
|
|
AxisMarks(position: .leading) { _ in
|
|
AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5))
|
|
.foregroundStyle(Color.gray.opacity(0.3))
|
|
AxisValueLabel()
|
|
.font(.caption2)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|