add measurement data views with charts for all measurement types
This commit is contained in:
parent
d061a17e54
commit
f5394d01ca
|
|
@ -0,0 +1,566 @@
|
||||||
|
/// Measurement data viewer — switches on type to show appropriate charts.
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Charts
|
||||||
|
|
||||||
|
// MARK: - Router
|
||||||
|
|
||||||
|
struct MeasurementDataView: View {
|
||||||
|
let measurement: Measurement
|
||||||
|
|
||||||
|
@State private var points: [DataPoint] = []
|
||||||
|
@State private var loaded = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if !loaded {
|
||||||
|
ProgressView()
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(typeLabel)
|
||||||
|
.onAppear { loadPoints() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadPoints() {
|
||||||
|
guard let mid = measurement.id else { return }
|
||||||
|
points = (try? Storage.shared.fetchDataPoints(measurementId: mid)) ?? []
|
||||||
|
loaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var content: some View {
|
||||||
|
switch MeasurementType(rawValue: measurement.type) {
|
||||||
|
case .eis:
|
||||||
|
EisDataView(points: decodePoints(EisPoint.self))
|
||||||
|
case .lsv:
|
||||||
|
LsvDataView(points: decodePoints(LsvPoint.self))
|
||||||
|
case .amp:
|
||||||
|
AmpDataView(points: decodePoints(AmpPoint.self))
|
||||||
|
case .chlorine:
|
||||||
|
ClDataView(
|
||||||
|
points: decodePoints(ClPoint.self),
|
||||||
|
result: decodeResult(ClResult.self)
|
||||||
|
)
|
||||||
|
case .ph:
|
||||||
|
PhDataView(result: decodeResult(PhResult.self))
|
||||||
|
case nil:
|
||||||
|
Text("Unknown type: \(measurement.type)")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var typeLabel: String {
|
||||||
|
switch measurement.type {
|
||||||
|
case "eis": "EIS"
|
||||||
|
case "lsv": "LSV"
|
||||||
|
case "amp": "Amperometry"
|
||||||
|
case "chlorine": "Chlorine"
|
||||||
|
case "ph": "pH"
|
||||||
|
default: measurement.type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func decodePoints<T: Decodable>(_ type: T.Type) -> [T] {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
return points.compactMap { try? decoder.decode(T.self, from: $0.payload) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func decodeResult<T: Decodable>(_ type: T.Type) -> T? {
|
||||||
|
guard let data = measurement.resultSummary else { return nil }
|
||||||
|
return try? JSONDecoder().decode(T.self, from: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - EIS data view
|
||||||
|
|
||||||
|
enum EisPlotMode: String, CaseIterable, Identifiable {
|
||||||
|
case nyquist = "Nyquist"
|
||||||
|
case bodeMag = "Bode |Z|"
|
||||||
|
case bodePhase = "Bode Phase"
|
||||||
|
|
||||||
|
var id: String { rawValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EisDataView: View {
|
||||||
|
let points: [EisPoint]
|
||||||
|
@State private var plotMode: EisPlotMode = .nyquist
|
||||||
|
@State private var showTable = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Picker("Plot", selection: $plotMode) {
|
||||||
|
ForEach(EisPlotMode.allCases) { mode in
|
||||||
|
Text(mode.rawValue).tag(mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
if points.isEmpty {
|
||||||
|
noData
|
||||||
|
} else {
|
||||||
|
plotView
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
if showTable && !points.isEmpty {
|
||||||
|
Divider()
|
||||||
|
eisTable
|
||||||
|
.frame(maxHeight: 250)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
Button(showTable ? "Hide Table" : "Show Table") {
|
||||||
|
showTable.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var plotView: some View {
|
||||||
|
switch plotMode {
|
||||||
|
case .nyquist:
|
||||||
|
nyquistChart
|
||||||
|
.padding()
|
||||||
|
case .bodeMag:
|
||||||
|
bodeMagChart
|
||||||
|
.padding()
|
||||||
|
case .bodePhase:
|
||||||
|
bodePhaseChart
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var nyquistChart: some View {
|
||||||
|
Chart {
|
||||||
|
ForEach(Array(points.enumerated()), id: \.offset) { _, pt in
|
||||||
|
PointMark(
|
||||||
|
x: .value("Z'", Double(pt.zReal)),
|
||||||
|
y: .value("-Z''", Double(-pt.zImag))
|
||||||
|
)
|
||||||
|
.foregroundStyle(Color(red: 0.4, green: 1, blue: 0.4))
|
||||||
|
.symbolSize(20)
|
||||||
|
}
|
||||||
|
ForEach(Array(points.enumerated()), id: \.offset) { _, pt in
|
||||||
|
LineMark(
|
||||||
|
x: .value("Z'", Double(pt.zReal)),
|
||||||
|
y: .value("-Z''", Double(-pt.zImag))
|
||||||
|
)
|
||||||
|
.foregroundStyle(Color(red: 0.4, green: 1, blue: 0.4).opacity(0.6))
|
||||||
|
.lineStyle(StrokeStyle(lineWidth: 1.5))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.chartXAxisLabel("Z' (Ohm)")
|
||||||
|
.chartYAxisLabel("-Z'' (Ohm)")
|
||||||
|
.chartXAxis { darkAxis }
|
||||||
|
.chartYAxis { darkAxisLeading }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var bodeMagChart: some View {
|
||||||
|
Chart {
|
||||||
|
ForEach(Array(points.enumerated()), id: \.offset) { _, pt in
|
||||||
|
LineMark(
|
||||||
|
x: .value("Freq", log10(max(Double(pt.freqHz), 1))),
|
||||||
|
y: .value("|Z|", log10(max(Double(pt.magOhms), 0.01)))
|
||||||
|
)
|
||||||
|
.foregroundStyle(Color.cyan)
|
||||||
|
.lineStyle(StrokeStyle(lineWidth: 2))
|
||||||
|
}
|
||||||
|
ForEach(Array(points.enumerated()), id: \.offset) { _, pt in
|
||||||
|
PointMark(
|
||||||
|
x: .value("Freq", log10(max(Double(pt.freqHz), 1))),
|
||||||
|
y: .value("|Z|", log10(max(Double(pt.magOhms), 0.01)))
|
||||||
|
)
|
||||||
|
.foregroundStyle(Color.cyan)
|
||||||
|
.symbolSize(16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.chartXAxisLabel("log10(Freq Hz)")
|
||||||
|
.chartYAxisLabel("log10(|Z| Ohm)")
|
||||||
|
.chartXAxis { darkAxis }
|
||||||
|
.chartYAxis { darkAxisLeading }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var bodePhaseChart: some View {
|
||||||
|
Chart {
|
||||||
|
ForEach(Array(points.enumerated()), id: \.offset) { _, pt in
|
||||||
|
LineMark(
|
||||||
|
x: .value("Freq", log10(max(Double(pt.freqHz), 1))),
|
||||||
|
y: .value("Phase", Double(pt.phaseDeg))
|
||||||
|
)
|
||||||
|
.foregroundStyle(Color.orange)
|
||||||
|
.lineStyle(StrokeStyle(lineWidth: 2))
|
||||||
|
}
|
||||||
|
ForEach(Array(points.enumerated()), id: \.offset) { _, pt in
|
||||||
|
PointMark(
|
||||||
|
x: .value("Freq", log10(max(Double(pt.freqHz), 1))),
|
||||||
|
y: .value("Phase", Double(pt.phaseDeg))
|
||||||
|
)
|
||||||
|
.foregroundStyle(Color.orange)
|
||||||
|
.symbolSize(16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.chartXAxisLabel("log10(Freq Hz)")
|
||||||
|
.chartYAxisLabel("Phase (deg)")
|
||||||
|
.chartXAxis { darkAxis }
|
||||||
|
.chartYAxis { darkAxisLeading }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var eisTable: some View {
|
||||||
|
MeasurementTable(
|
||||||
|
columns: [
|
||||||
|
MeasurementColumn(header: "Freq (Hz)", width: 80, alignment: .trailing),
|
||||||
|
MeasurementColumn(header: "|Z| (Ohm)", width: 90, alignment: .trailing),
|
||||||
|
MeasurementColumn(header: "Phase", width: 70, alignment: .trailing),
|
||||||
|
MeasurementColumn(header: "Re", width: 80, alignment: .trailing),
|
||||||
|
MeasurementColumn(header: "Im", width: 80, alignment: .trailing),
|
||||||
|
],
|
||||||
|
rows: points,
|
||||||
|
cellText: { pt, col in
|
||||||
|
switch col {
|
||||||
|
case 0: String(format: "%.1f", pt.freqHz)
|
||||||
|
case 1: String(format: "%.2f", pt.magOhms)
|
||||||
|
case 2: String(format: "%.2f", pt.phaseDeg)
|
||||||
|
case 3: String(format: "%.2f", pt.zReal)
|
||||||
|
case 4: String(format: "%.2f", pt.zImag)
|
||||||
|
default: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var noData: some View {
|
||||||
|
Text("No data points")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - LSV data view
|
||||||
|
|
||||||
|
struct LsvDataView: View {
|
||||||
|
let points: [LsvPoint]
|
||||||
|
@State private var showTable = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
if points.isEmpty {
|
||||||
|
noData
|
||||||
|
} else {
|
||||||
|
ivChart
|
||||||
|
.padding()
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
if showTable && !points.isEmpty {
|
||||||
|
Divider()
|
||||||
|
lsvTable
|
||||||
|
.frame(maxHeight: 250)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
Button(showTable ? "Hide Table" : "Show Table") {
|
||||||
|
showTable.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var ivChart: some View {
|
||||||
|
Chart {
|
||||||
|
ForEach(Array(points.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(points.enumerated()), id: \.offset) { _, pt in
|
||||||
|
PointMark(
|
||||||
|
x: .value("V", Double(pt.vMv)),
|
||||||
|
y: .value("I", Double(pt.iUa))
|
||||||
|
)
|
||||||
|
.foregroundStyle(Color.yellow)
|
||||||
|
.symbolSize(16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.chartXAxisLabel("Voltage (mV)")
|
||||||
|
.chartYAxisLabel("Current (uA)")
|
||||||
|
.chartXAxis { darkAxis }
|
||||||
|
.chartYAxis { darkAxisLeading }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lsvTable: some View {
|
||||||
|
MeasurementTable(
|
||||||
|
columns: [
|
||||||
|
MeasurementColumn(header: "V (mV)", width: 80, alignment: .trailing),
|
||||||
|
MeasurementColumn(header: "I (uA)", width: 90, alignment: .trailing),
|
||||||
|
],
|
||||||
|
rows: points,
|
||||||
|
cellText: { pt, col in
|
||||||
|
switch col {
|
||||||
|
case 0: String(format: "%.1f", pt.vMv)
|
||||||
|
case 1: String(format: "%.3f", pt.iUa)
|
||||||
|
default: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var noData: some View {
|
||||||
|
Text("No data points")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Amperometry data view
|
||||||
|
|
||||||
|
struct AmpDataView: View {
|
||||||
|
let points: [AmpPoint]
|
||||||
|
@State private var showTable = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
if points.isEmpty {
|
||||||
|
noData
|
||||||
|
} else {
|
||||||
|
ampChart
|
||||||
|
.padding()
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
if showTable && !points.isEmpty {
|
||||||
|
Divider()
|
||||||
|
ampTable
|
||||||
|
.frame(maxHeight: 250)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
Button(showTable ? "Hide Table" : "Show Table") {
|
||||||
|
showTable.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var ampChart: some View {
|
||||||
|
let ampColor = Color(red: 0.6, green: 0.6, blue: 1.0)
|
||||||
|
return Chart {
|
||||||
|
ForEach(Array(points.enumerated()), id: \.offset) { _, pt in
|
||||||
|
LineMark(
|
||||||
|
x: .value("t", Double(pt.tMs)),
|
||||||
|
y: .value("I", Double(pt.iUa))
|
||||||
|
)
|
||||||
|
.foregroundStyle(ampColor)
|
||||||
|
.lineStyle(StrokeStyle(lineWidth: 2))
|
||||||
|
}
|
||||||
|
ForEach(Array(points.enumerated()), id: \.offset) { _, pt in
|
||||||
|
PointMark(
|
||||||
|
x: .value("t", Double(pt.tMs)),
|
||||||
|
y: .value("I", Double(pt.iUa))
|
||||||
|
)
|
||||||
|
.foregroundStyle(ampColor)
|
||||||
|
.symbolSize(16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.chartXAxisLabel("Time (ms)")
|
||||||
|
.chartYAxisLabel("Current (uA)")
|
||||||
|
.chartXAxis { darkAxis }
|
||||||
|
.chartYAxis { darkAxisLeading }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var ampTable: some View {
|
||||||
|
MeasurementTable(
|
||||||
|
columns: [
|
||||||
|
MeasurementColumn(header: "t (ms)", width: 80, alignment: .trailing),
|
||||||
|
MeasurementColumn(header: "I (uA)", width: 90, alignment: .trailing),
|
||||||
|
],
|
||||||
|
rows: points,
|
||||||
|
cellText: { pt, col in
|
||||||
|
switch col {
|
||||||
|
case 0: String(format: "%.1f", pt.tMs)
|
||||||
|
case 1: String(format: "%.3f", pt.iUa)
|
||||||
|
default: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var noData: some View {
|
||||||
|
Text("No data points")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Chlorine data view
|
||||||
|
|
||||||
|
struct ClDataView: View {
|
||||||
|
let points: [ClPoint]
|
||||||
|
let result: ClResult?
|
||||||
|
@State private var showTable = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
if let r = result {
|
||||||
|
resultBanner(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
if points.isEmpty {
|
||||||
|
noData
|
||||||
|
} else {
|
||||||
|
clChart
|
||||||
|
.padding()
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
if showTable && !points.isEmpty {
|
||||||
|
Divider()
|
||||||
|
clTable
|
||||||
|
.frame(maxHeight: 250)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
Button(showTable ? "Hide Table" : "Show Table") {
|
||||||
|
showTable.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resultBanner(_ r: ClResult) -> some View {
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
Text(String(format: "Free: %.3f uA", r.iFreeUa))
|
||||||
|
.foregroundStyle(Color(red: 0.2, green: 1, blue: 0.5))
|
||||||
|
Text(String(format: "Total: %.3f uA", r.iTotalUa))
|
||||||
|
.foregroundStyle(Color(red: 1, green: 0.6, blue: 0.2))
|
||||||
|
Text(String(format: "Combined: %.3f uA", r.iTotalUa - r.iFreeUa))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.font(.subheadline.monospacedDigit())
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var clChart: some View {
|
||||||
|
Chart {
|
||||||
|
ForEach(Array(points.enumerated()), id: \.offset) { _, pt in
|
||||||
|
LineMark(
|
||||||
|
x: .value("t", Double(pt.tMs)),
|
||||||
|
y: .value("I", Double(pt.iUa))
|
||||||
|
)
|
||||||
|
.foregroundStyle(by: .value("Phase", phaseLabel(pt.phase)))
|
||||||
|
.lineStyle(StrokeStyle(lineWidth: 2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.chartForegroundStyleScale([
|
||||||
|
"Conditioning": Color.gray,
|
||||||
|
"Free": Color(red: 0.2, green: 1, blue: 0.5),
|
||||||
|
"Total": Color(red: 1, green: 0.6, blue: 0.2),
|
||||||
|
])
|
||||||
|
.chartXAxisLabel("Time (ms)")
|
||||||
|
.chartYAxisLabel("Current (uA)")
|
||||||
|
.chartXAxis { darkAxis }
|
||||||
|
.chartYAxis { darkAxisLeading }
|
||||||
|
.chartLegend(position: .top)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func phaseLabel(_ phase: UInt8) -> String {
|
||||||
|
switch phase {
|
||||||
|
case 1: "Free"
|
||||||
|
case 2: "Total"
|
||||||
|
default: "Conditioning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var clTable: some View {
|
||||||
|
MeasurementTable(
|
||||||
|
columns: [
|
||||||
|
MeasurementColumn(header: "t (ms)", width: 80, alignment: .trailing),
|
||||||
|
MeasurementColumn(header: "I (uA)", width: 90, alignment: .trailing),
|
||||||
|
MeasurementColumn(header: "Phase", width: 70, alignment: .trailing),
|
||||||
|
],
|
||||||
|
rows: points,
|
||||||
|
cellText: { pt, col in
|
||||||
|
switch col {
|
||||||
|
case 0: String(format: "%.1f", pt.tMs)
|
||||||
|
case 1: String(format: "%.3f", pt.iUa)
|
||||||
|
case 2: phaseLabel(pt.phase)
|
||||||
|
default: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var noData: some View {
|
||||||
|
Text("No data points")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - pH data view
|
||||||
|
|
||||||
|
struct PhDataView: View {
|
||||||
|
let result: PhResult?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
if let r = result {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text(String(format: "pH: %.2f", r.ph))
|
||||||
|
.font(.system(size: 56, weight: .bold, design: .rounded))
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
|
||||||
|
Text(String(format: "OCP: %.1f mV", r.vOcpMv))
|
||||||
|
.font(.title3)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
Text(String(format: "Temperature: %.1f C", r.tempC))
|
||||||
|
.font(.title3)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||||||
|
.padding()
|
||||||
|
} else {
|
||||||
|
Text("No pH result")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Shared axis styles
|
||||||
|
|
||||||
|
private var darkAxis: some AxisContent {
|
||||||
|
AxisMarks { _ in
|
||||||
|
AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5))
|
||||||
|
.foregroundStyle(Color.gray.opacity(0.3))
|
||||||
|
AxisValueLabel()
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var darkAxisLeading: some AxisContent {
|
||||||
|
AxisMarks(position: .leading) { _ in
|
||||||
|
AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5))
|
||||||
|
.foregroundStyle(Color.gray.opacity(0.3))
|
||||||
|
AxisValueLabel()
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue