diff --git a/cue-ios/CueIOS/AppState.swift b/cue-ios/CueIOS/AppState.swift index e3243e2..b52ef46 100644 --- a/cue-ios/CueIOS/AppState.swift +++ b/cue-ios/CueIOS/AppState.swift @@ -37,6 +37,8 @@ final class AppState { // LSV var lsvPoints: [LsvPoint] = [] + var lsvPeaks: [LsvPeak] = [] + var lsvManualPeaks: Bool = false var lsvTotal: UInt16 = 0 var lsvStartV: String = "0" var lsvStopV: String = "500" @@ -163,6 +165,9 @@ final class AppState { case .lsvEnd: saveLsv() + if !lsvManualPeaks { + lsvPeaks = detectLsvPeaks(lsvPoints) + } status = "LSV complete: \(lsvPoints.count) points" case .ampStart(let vHold): diff --git a/cue-ios/CueIOS/Views/LSVView.swift b/cue-ios/CueIOS/Views/LSVView.swift index 5bb2f5f..503282b 100644 --- a/cue-ios/CueIOS/Views/LSVView.swift +++ b/cue-ios/CueIOS/Views/LSVView.swift @@ -7,6 +7,7 @@ struct LSVView: View { var body: some View { VStack(spacing: 0) { controlsRow + peakLabels Divider() GeometryReader { geo in if geo.size.width > 700 { @@ -42,6 +43,16 @@ struct LSVView: View { Button("Start LSV") { state.startLSV() } .buttonStyle(ActionButtonStyle(color: .green)) + + Button(state.lsvManualPeaks ? "Manual" : "Auto") { + state.lsvManualPeaks.toggle() + if state.lsvManualPeaks { + state.lsvPeaks.removeAll() + } else { + state.lsvPeaks = detectLsvPeaks(state.lsvPoints) + } + } + .font(.caption) } .padding(.horizontal) .padding(.vertical, 8) @@ -86,6 +97,18 @@ struct LSVView: View { .foregroundStyle(Color.yellow) .symbolSize(16) } + ForEach(Array(state.lsvPeaks.enumerated()), id: \.offset) { _, peak in + PointMark( + x: .value("V", Double(peak.vMv)), + y: .value("I", Double(peak.iUa)) + ) + .foregroundStyle(peakColor(peak.kind)) + .symbolSize(100) + .symbol(.diamond) + RuleMark(x: .value("V", Double(peak.vMv))) + .foregroundStyle(peakColor(peak.kind).opacity(0.3)) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [4, 4])) + } } .chartXAxisLabel("V (mV)") .chartYAxisLabel("I (uA)", position: .leading) @@ -115,6 +138,36 @@ struct LSVView: View { .clipShape(RoundedRectangle(cornerRadius: 8)) } + @ViewBuilder + private var peakLabels: some View { + if !state.lsvPeaks.isEmpty { + HStack(spacing: 12) { + ForEach(Array(state.lsvPeaks.enumerated()), id: \.offset) { _, peak in + let label: String = { + switch peak.kind { + case .freeCl: return String(format: "Free: %.0fmV %.2fuA", peak.vMv, peak.iUa) + case .totalCl: return String(format: "Total: %.0fmV %.2fuA", peak.vMv, peak.iUa) + case .crossover: return String(format: "X-over: %.0fmV", peak.vMv) + } + }() + Text(label) + .font(.caption) + .foregroundStyle(peakColor(peak.kind)) + } + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + } + } + + private func peakColor(_ kind: PeakKind) -> Color { + switch kind { + case .freeCl: .green + case .totalCl: .orange + case .crossover: .purple + } + } + // MARK: - Table private var lsvTable: some View {