iOS: add LSV sweep and voltammogram to chlorine tab

This commit is contained in:
jess 2026-04-02 23:11:43 -07:00
parent 73899beaa5
commit 34f8bda191
1 changed files with 115 additions and 2 deletions

View File

@ -7,11 +7,13 @@ struct ChlorineView: View {
var body: some View {
VStack(spacing: 0) {
controlsRow
clPeakLabels
Divider()
GeometryReader { geo in
if geo.size.width > 700 {
HSplitLayout(ratio: 0.55) {
VStack(spacing: 4) {
voltammogramPlot
resultBanner
chlorinePlot
}
@ -21,8 +23,9 @@ struct ChlorineView: View {
} else {
ScrollView {
VStack(spacing: 12) {
voltammogramPlot.frame(height: 250)
resultBanner
chlorinePlot.frame(height: 350)
chlorinePlot.frame(height: 250)
clTable.frame(height: 300)
}
.padding()
@ -37,6 +40,21 @@ struct ChlorineView: View {
private var controlsRow: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
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)
Divider().frame(height: 24)
LabeledField("Cond mV", text: $state.clCondV, width: 70)
LabeledField("Cond ms", text: $state.clCondT, width: 70)
LabeledField("Free mV", text: $state.clFreeV, width: 70)
@ -88,7 +106,102 @@ struct ChlorineView: View {
}
}
// MARK: - Plot
// MARK: - Peak labels
@ViewBuilder
private var clPeakLabels: 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(clPeakColor(peak.kind))
}
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
}
}
private func clPeakColor(_ kind: PeakKind) -> Color {
switch kind {
case .freeCl: .green
case .totalCl: .orange
case .crossover: .purple
}
}
// MARK: - Voltammogram
private var voltammogramPlot: some View {
Group {
if state.lsvPoints.isEmpty {
Text("No LSV data")
.foregroundStyle(Color(white: 0.4))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black.opacity(0.3))
} else {
PlotContainer(title: "") {
Chart {
if let ref = state.lsvRef {
ForEach(Array(ref.enumerated()), id: \.offset) { _, pt in
LineMark(x: .value("V", Double(pt.vMv)), y: .value("I", Double(pt.iUa)))
.foregroundStyle(Color.gray.opacity(0.5))
.lineStyle(StrokeStyle(lineWidth: 1.5))
}
}
ForEach(Array(state.lsvPoints.enumerated()), id: \.offset) { _, pt in
LineMark(x: .value("V", Double(pt.vMv)), y: .value("I", Double(pt.iUa)))
.foregroundStyle(Color.yellow)
.lineStyle(StrokeStyle(lineWidth: 2))
}
ForEach(Array(state.lsvPoints.enumerated()), id: \.offset) { _, pt in
PointMark(x: .value("V", Double(pt.vMv)), y: .value("I", Double(pt.iUa)))
.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(clPeakColor(peak.kind))
.symbolSize(100)
.symbol(.diamond)
RuleMark(x: .value("V", Double(peak.vMv)))
.foregroundStyle(clPeakColor(peak.kind).opacity(0.3))
.lineStyle(StrokeStyle(lineWidth: 1, dash: [4, 4]))
}
}
.chartXAxisLabel("V (mV)")
.chartYAxisLabel("I (uA)", position: .leading)
.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(Color.yellow)
}
}
.padding(8)
}
}
}
.background(Color.black.opacity(0.3))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
// MARK: - Chlorine plot
private var chlorinePlot: some View {
Group {