6

In Swift Charts the signature for chartForegroundStyleScale to set the ShapeStyle for each data series is:

func chartForegroundStyleScale<DataValue, S>(_ mapping: KeyValuePairs<DataValue, S>) -> some View where DataValue : Plottable, S : ShapeStyle

The KeyValuePairs initialiser (init(dictionaryLiteral: (Key, Value)...)) only takes a variadic parameter so any attempt to initialise a foreground style from an array (in my case <String, Color>) results in the error:

Cannot pass array of type '[(String, Color)]' as variadic arguments of type '(String, Color)'

In my application the names of the chart series are set dynamically from the data so although I can generate a [String : Color] dictionary or an array of (String, Color) tuples I can't see that it's possible to pass either of these into chartForegroundStyleScale? Unless I'm missing something this seems like a odd limitation in Swift charts that the series names need to be hard coded for this modifier?

Fleet Phil
  • 333
  • 2
  • 12

2 Answers2

6

You could also pass an array of colors to .chartForegroundStyleScale(range:). As long as you add the colors to the array in the same order you add your graph marks it should work fine.

Not incredibly elegant either, but this approach works with an arbitrary number or entries.


struct GraphItem: Identifiable {
    var id = UUID()
    var label: String
    var value: Double
    var color: Color
}

struct ContentView: View {
    
    let data = [
        GraphItem(label: "Apples", value: 2, color: .red),
        GraphItem(label: "Pears", value: 3, color: .yellow),
        GraphItem(label: "Melons", value: 5, color: .green)
    ]
    
    var body: some View {
        Chart {
            ForEach(data, id: \.label) { item in
                BarMark(
                    x: .value("Count", item.value),
                    y: .value("Fruit", item.label)
                )
                .foregroundStyle(by: .value("Fruit", item.label))
            }
        }
        .chartForegroundStyleScale(range: graphColors(for: data))
    }
    
    func graphColors(for input: [GraphItem]) -> [Color] {
        var returnColors = [Color]()
        for item in input {
            returnColors.append(item.color)
        }
        return returnColors
    }
}

carloe
  • 1,440
  • 3
  • 18
  • 26
  • 1
    Doh!! As all the examples in the documentation use the `KeyValuePairs initialiser` for `chartForegroundStyleScale` I had completely missed that there are several other initialisers with more flexible options including ```func chartForegroundStyleScale(mapping: @escaping (DataValue) -> S) -> some View where DataValue : Plottable, S : ShapeStyle``` . This gives me exactly what I need - a function that can generate the values dynamically – Fleet Phil Oct 22 '22 at 14:22
2

OK I've found an approach that works as long as an arbitrary limitation to the number of entries is acceptable (example below with max size of 4:

func keyValuePairs<S, T>(_ from: [(S, T)]) -> KeyValuePairs<S, T> {
    switch from.count {
    case 1: return [ from[0].0 : from[0].1 ]
    case 2: return [ from[0].0 : from[0].1, from[1].0 : from[1].1 ]
    case 3: return [ from[0].0 : from[0].1, from[1].0 : from[1].1, from[2].0 : from[2].1 ]
    default: return [ from[0].0 : from[0].1, from[1].0 : from[1].1, from[2].0 : from[2].1, from[3].0 : from[3].1 ]
}

In my case I know that there won't be more than 20 mappings so this func can just be extended to accommodate that number. Not ideal, but it works...

Fleet Phil
  • 333
  • 2
  • 12