Fleet Phil
Fleet Phil

Reputation: 461

How can I create a dynamic input to .chartForegroundStyleScale

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?

Upvotes: 12

Views: 2901

Answers (4)

thecoolwinter
thecoolwinter

Reputation: 1009

Finally found it. You can use chartForegroundStyleScale(domain:mapping:) . With this you pass in the "domain", all values your key can have and a callback that maps a key to a style.

This is preferable to the upvoted answer, in cases where you may not have all data types in the chart at each x value. For instance, a chart that has many stacked bar values that don't have each value at each x position.

Upvotes: 1

Teemu Sakari
Teemu Sakari

Reputation: 21

You can also write the function to the .chartForegroundStyleScale(range:) itself:

.chartForegroundStyleScale({ 
  if something { 
    ["MyString": myColor] 
  } else { 
    [:] 
  }
}())

Upvotes: -1

carloe
carloe

Reputation: 1540

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
    }
}

Upvotes: 13

Fleet Phil
Fleet Phil

Reputation: 461

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...

Upvotes: 2

Related Questions