How can I use a variable to choose which vector(?) in my struct to plot?

67 Views Asked by At

I am trying to reuse a SwiftUI Swift Chart view instead of having six separately defined Chart views. (The six separate charts are working fine)

Q: How does one use a var to represent different "columns" of data from my array of structs I want to be plotted.

(element.xGyroelement.yGyro ) in the y: .value("xGyro", element.xGyro)

To reduce code duplication, I'd rather not use a switch/case statement for the whole Chart {} block.

I did try using a switch/case for just the y: .value... line but I got an error.

What I don't know how to do is replace the (currently) hard-coded element.xGyro with a variable that I can pass in to plot different points [ xGyro | yGyro | zGyro | xAccel | yAccel | zAccel ] in the following line with a variable.

y: .value("xGyro", element.xGyro)

The source for my charts is an array of structs MotionDataPointClass.swift:

class MotionDataPointM: Identifiable {
   var xGyro: Double
   var yGyro: Double
   var zGyro: Double
   var xAccel: Double
   var yAccel: Double
   var zAccel: Double
   var timeStamp: Date
   var myIndex: Int
   var id: Date { timeStamp }
}

The chart code is SingleChartView.swift:

import os
import SwiftUI
import CoreMotion
import CoreLocation
import Charts

struct SingleChartView: View {
   @ObservedObject var locationsHandler = LocationsHandler.shared
   let motionDataPoints: [MotionDataPointM]
   
   let numberOfPointsToPlot: Int
   let vertScaleMin: Double
   let vertScaleMax: Double
   let cornerLabel: String
   let lineColor: Color
   let valuesToPlot: Int
   
   var body: some View {
      Chart{
         ForEach(motionDataPoints.suffix(numberOfPointsToPlot)) { element in
            LineMark(
               x: .value("Date", element.timeStamp),

               y: .value("xGyro", element.xGyro) // <-- HERE
            )
         }
         .interpolationMethod(.catmullRom)
      }
      .foregroundStyle(lineColor)
      .chartYScale(domain: [vertScaleMin, vertScaleMax])
      .padding(.top, 10.0)
      .padding(.bottom, 3.0)
      .padding(.horizontal, 3.0)
      .overlay(
         RoundedRectangle(cornerRadius: 10)
            .stroke(Color.purple, lineWidth: 1.0)
      )
      .overlay(
         HStack {
            VStack {
               Text("\(cornerLabel)")
                  .font(.largeTitle)
                  .fontWeight(.black)
                  .foregroundColor(lineColor)
                  .opacity(0.3)
                  .padding(.leading, 4)
                  .padding(.top, -3)
               Spacer()
            }
            Spacer()
         }
      )
   }
}

I am calling/invoking the chart from my ContentView.swift file:

SingleChartView(motionDataPoints: motionDataPoints, numberOfPointsToPlot: numberOfPointsToPlot, vertScaleMin: gyroMinX, vertScaleMax: gyroMaxX, cornerLabel: "W", lineColor: Color("YChartLines"), valuesToPlot: vectorToPlot.xGyro)

I defined an enum to represent the different columns/vectors in the data struct:

enum vectorToPlot {
   public static let xGyro = 1
   public static let yGyro = 2
   public static let zGyro = 3
   public static let xAccel = 4
   public static let yAccel = 5
   public static let zAccel = 6
}
1

There are 1 best solutions below

0
Sweeper On BEST ANSWER

The data you want to plot just so happens to all be Doubles, so you can use a KeyPath<MotionDataPointM, Double> to represent the property you want to plot.

Here is an example:

@State var data = [
    // dummy data...
    MotionDataPointM(),
    MotionDataPointM(),
    MotionDataPointM(),
    MotionDataPointM(),
    MotionDataPointM(),
]

@State var keyPath: KeyPath<MotionDataPointM, Double> = \.xGyro
// or just 'let keyPath: KeyPath<MotionDataPointM, Double>'
// if you want to pass this from the superview

var body: some View {
    Chart(data) { datum in
        LineMark(
            x: .value("Date", datum.timeStamp),
            // here is how you get the property
            y: .value("Y", datum[keyPath: keyPath])
        )
    }
    // as an example, some buttons to change which property is plotted
    HStack {
        Button("xGyro") {
            keyPath = \.xGyro
        }
        Button("yGyro") {
            keyPath = \.yGyro
        }
        Button("zGyro") {
            keyPath = \.zGyro
        }
    }
}

For some reason, key paths don't work well in things like the selection of a Picker, so you might want to keep your enum, and keep a mapping from your enum to the KeyPaths.

If the properties you want to plot are not all the same type, you would need to a switch to produce LineMarks, or you can make SingleChartView generic, if the property each chart plots is constant:

struct SingleChartView<Value: Plottable>: View {

    let keyPath: KeyPath<MotionDataPointM, Value>

    // ...
}